diff --git a/.gitignore b/.gitignore
index d829c0067e..a3ce6febfa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,8 @@ Release
*.log
core.*
!core.png
+!core.py
+
+# python files
+*.egg-info
+__pycache__
diff --git a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java b/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
index 3ee4ab24e9..f1ce702bf6 100644
--- a/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
+++ b/Ghidra/Features/Base/ghidra_scripts/VSCodeProjectScript.java
@@ -65,6 +65,7 @@ public class VSCodeProjectScript extends GhidraScript {
writeSettings(installDir, projectDir, classpathSourceMap);
writeLaunch(installDir, projectDir, classpathSourceMap);
writeSampleScriptJava(projectDir);
+ writeSampleScriptPyhidra(projectDir);
writeSampleModule(installDir, projectDir);
println("Successfully created VSCode project directory at: " + projectDir);
@@ -226,6 +227,25 @@ public class VSCodeProjectScript extends GhidraScript {
}
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
}
+
+ private void writeSampleScriptPyhidra(File projectDir) throws IOException {
+ File scriptsDir = new File(projectDir, "ghidra_scripts");
+ File scriptFile = new File(scriptsDir, "sample_script.py");
+ String sampleScript = """
+ # Sample Pyhidra GhidraScript
+ # @category Examples
+ # @runtime Pyhidra
+
+ from java.util import LinkedList
+ java_list = LinkedList([1,2,3])
+
+ block = currentProgram.memory.getBlock('.text')
+ """;
+ if (!FileUtilities.mkdirs(scriptFile.getParentFile())) {
+ throw new IOException("Failed to create: " + scriptFile.getParentFile());
+ }
+ FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
+ }
/**
* Write a sample Java-based Ghidra module into the VSCode project directory
diff --git a/Ghidra/Features/Pyhidra/.gitignore b/Ghidra/Features/Pyhidra/.gitignore
new file mode 100644
index 0000000000..adc16ce4d2
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.gitignore
@@ -0,0 +1 @@
+/.pytest_cache/
diff --git a/Ghidra/Features/Pyhidra/.launch/Ghidra Attach.launch b/Ghidra/Features/Pyhidra/.launch/Ghidra Attach.launch
new file mode 100644
index 0000000000..ca245cdfe9
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/Ghidra Attach.launch
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI Debug.launch b/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI Debug.launch
new file mode 100644
index 0000000000..8ded57cdb0
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI Debug.launch
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI.launch b/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI.launch
new file mode 100644
index 0000000000..560206411b
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/Pyhidra GUI.launch
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter Debug.launch b/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter Debug.launch
new file mode 100644
index 0000000000..776a77b978
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter Debug.launch
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter.launch b/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter.launch
new file mode 100644
index 0000000000..ba7189fa7f
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter.launch
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/_Pyhidra GUI Debug.launch b/Ghidra/Features/Pyhidra/.launch/_Pyhidra GUI Debug.launch
new file mode 100644
index 0000000000..ce6849d1d1
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/_Pyhidra GUI Debug.launch
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/.launch/_Pyhidra Interpreter Debug.launch b/Ghidra/Features/Pyhidra/.launch/_Pyhidra Interpreter Debug.launch
new file mode 100644
index 0000000000..0afdfb09b6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/.launch/_Pyhidra Interpreter Debug.launch
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/Module.manifest b/Ghidra/Features/Pyhidra/Module.manifest
new file mode 100644
index 0000000000..24a3826063
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/Module.manifest
@@ -0,0 +1,15 @@
+EXCLUDE FROM GHIDRA JAR: true
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-win_amd64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-win_amd64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-win_amd64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp39-cp39-win_amd64.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0.tar.gz Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/packaging-23.2-py3-none-any.whl Apache License 2.0
+MODULE FILE LICENSE: pypkg/dist/setuptools-68.0.0-py3-none-any.whl MIT
diff --git a/Ghidra/Features/Pyhidra/build.gradle b/Ghidra/Features/Pyhidra/build.gradle
new file mode 100644
index 0000000000..79a102c146
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/build.gradle
@@ -0,0 +1,81 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
+apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
+apply from: "$rootProject.projectDir/gradle/helpProject.gradle"
+apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
+apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
+apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
+apply from: "${rootProject.projectDir}/gradle/hasPythonPackage.gradle"
+apply plugin: 'eclipse'
+
+eclipse.project.name = 'Features Pyhidra'
+
+
+dependencies {
+ api project(':Base')
+}
+
+
+// NOTE: The Python package is a "Pure Python" package. Building the wheel does not
+// require any dependencies except setuptools. Installing the wheel will require
+// the correct os/python version of Jpype and packaging. Installing the wheel does
+// not require Ghidra.
+distributePyDep("JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl")
+distributePyDep("JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
+distributePyDep("JPype1-1.5.0-cp310-cp310-win_amd64.whl")
+distributePyDep("JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl")
+distributePyDep("JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
+distributePyDep("JPype1-1.5.0-cp311-cp311-win_amd64.whl")
+distributePyDep("JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl")
+distributePyDep("JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
+distributePyDep("JPype1-1.5.0-cp312-cp312-win_amd64.whl")
+distributePyDep("JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
+distributePyDep("JPype1-1.5.0-cp39-cp39-win_amd64.whl")
+distributePyDep("JPype1-1.5.0.tar.gz")
+distributePyDep("packaging-23.2-py3-none-any.whl")
+distributePyDep("setuptools-68.0.0-py3-none-any.whl")
+
+// Install JPype into the development virtual environment
+task installJPype(type: Exec) {
+ dependsOn(":createPythonVirtualEnvironment")
+
+ File depsDir = file("${DEPS_DIR}/Pyhidra")
+ File binRepoDir = file("${BIN_REPO}/Ghidra/Features/Pyhidra")
+ def dir = depsDir.exists() ? depsDir : binRepoDir
+
+ commandLine "$PYTHON3_VENV", "-m", "pip", "install", "--no-index", "-f", "$dir", "JPype1"
+}
+
+// Install Pyhidra in editable mode to the development virtual environment
+task installEditablePyhidra(type: Exec) {
+ dependsOn("installJPype")
+
+ commandLine "$PYTHON3_VENV", "-m", "pip", "install", "-e", "src/main/py"
+}
+rootProject.prepDev.dependsOn installEditablePyhidra
+
+// Add pyhidraLauncher.py to the release
+rootProject.assembleDistribution {
+ dependsOn(buildPyPackage)
+ def p = this.project
+ def zipPath = getZipPath(p)
+ from (this.project.projectDir.toString()) {
+ include "pyhidraLauncher.py"
+ into { zipPath }
+ }
+}
+
diff --git a/Ghidra/Features/Pyhidra/certification.manifest b/Ghidra/Features/Pyhidra/certification.manifest
new file mode 100644
index 0000000000..423fbf567d
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/certification.manifest
@@ -0,0 +1,7 @@
+##VERSION: 2.0
+##MODULE IP: Apache License 2.0
+Module.manifest||GHIDRA||||END|
+data/python.theme.properties||GHIDRA||||END|
+src/main/help/help/TOC_Source.xml||GHIDRA||||END|
+src/main/help/help/topics/Pyhidra/interpreter.html||GHIDRA||||END|
+src/main/resources/images/python.png||GHIDRA||||END|
diff --git a/Ghidra/Features/Pyhidra/data/python.theme.properties b/Ghidra/Features/Pyhidra/data/python.theme.properties
new file mode 100644
index 0000000000..3400dc379c
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/data/python.theme.properties
@@ -0,0 +1,21 @@
+
+[Defaults]
+
+color.fg.plugin.python.syntax.class = color.palette.blue
+color.fg.plugin.python.syntax.code = color.palette.darkgreen
+color.fg.plugin.python.syntax.function = color.palette.green
+color.fg.plugin.python.syntax.instance = color.palette.purple
+color.fg.plugin.python.syntax.map = color.palette.steelblue
+color.fg.plugin.python.syntax.method = color.palette.teal
+color.fg.plugin.python.syntax.null = color.palette.red
+color.fg.plugin.python.syntax.number = color.palette.darkgray
+color.fg.plugin.python.syntax.package = color.palette.darkred
+color.fg.plugin.python.syntax.sequence = color.palette.saddlebrown
+color.fg.plugin.python.syntax.special = color.palette.darkgreen
+
+icon.plugin.python = python.png
+
+
+
+[Dark Defaults]
+
diff --git a/Ghidra/Features/Pyhidra/ghidra_scripts/PyhidraBasics.py b/Ghidra/Features/Pyhidra/ghidra_scripts/PyhidraBasics.py
new file mode 100644
index 0000000000..f018bd6ae6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/ghidra_scripts/PyhidraBasics.py
@@ -0,0 +1,77 @@
+# Examples of Pyhidra-specific functionality
+# @category: Examples.Python
+# @runtime Pyhidra
+
+
+# we can import java libraries just as if they were python libraries
+from java.util import LinkedList
+
+# and then use them like they are natural classes
+java_list = LinkedList([1,2,3])
+print(f"linked list object class: {java_list.__class__}")
+
+# importing and using Ghidra modules is the same
+from ghidra.program.flatapi import FlatProgramAPI
+print(f"max references to a flat program api: {FlatProgramAPI.MAX_REFERENCES_TO}")
+
+# we can also do normal python-ish things on our Java objects, like:
+# indexing
+print(f"first element of the list: {java_list[0]}")
+
+# slicing
+print(f"first two elements of the list: {java_list[0:2]}")
+
+# list comprehension
+java_list_double = [i * 2 for i in java_list]
+print(f"list comprehension result: {java_list_double}")
+
+# automatic calls to getters
+print(f"current program name: {currentProgram.name}") # calls currentProgram.getName()
+
+# here's an example of how this stuff might come in handy with Ghidra:
+print('current program memory blocks:\n')
+for block in currentProgram.memory.blocks:
+ print(block.name)
+
+
+# many Ghidra functions need a Java-native array to pass or receive values
+# JPype provides objects of JByte, JChar, etc. to meet this need
+# this example demonstrates how you would create an array of bytes to get
+# the first 10 bytes of memory from the .text section
+
+# we need this import to get at the helper classes
+import jpype
+
+# get the block we need
+block = currentProgram.memory.getBlock('.text')
+if block:
+ # the verbose way of getting the array
+ byte_array_maker = jpype.JArray(jpype.JByte)
+ byte_array = byte_array_maker(10)
+
+ # we also could have taken a shortcut with just:
+ # byte_array = jpype.JByte[10]
+
+ # let's have a look at our new object
+ print(f"array class: {byte_array.__class__}")
+ # will be
+ print(f"array length: {len(byte_array)}")
+
+ # we can now use this array wherever a Java method requires a byte[] type
+ # the signature of getBytes is getBytes(Address addr, byte[] b)
+ block.getBytes(block.start, byte_array)
+
+ # after the call, we can get the bytes out as desired
+ # we just put them in a list comprehension here
+ print(f"first 10 bytes of .text: {['%#x' % ((b+256)%256) for b in byte_array]}")
+
+ # if the data isn't being changed, a bytes-like objct may be used
+ data = b"Hello"
+ clearListing(block.start, block.start.add(len(data) - 1))
+ block.putBytes(block.start, data)
+
+else:
+ print('no block named .text in this program.')
+
+# see the user manual of JPype for more details on interoperability:
+# https://jpype.readthedocs.io/en/latest/userguide.html
diff --git a/Ghidra/Features/Pyhidra/pyhidraLauncher.py b/Ghidra/Features/Pyhidra/pyhidraLauncher.py
new file mode 100644
index 0000000000..ec0e9dd1b6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/pyhidraLauncher.py
@@ -0,0 +1,91 @@
+import argparse
+import os
+import sys
+import subprocess
+from pathlib import Path
+from typing import List
+from sys import stderr
+
+def upgrade(pip_args: List[str], dist_dir: Path, current_pyhidra_version: str) -> bool:
+ from packaging.version import Version # if pyhidra imported, we know we have packaging
+ included_pyhidra: Path = next(dist_dir.glob('pyhidra-*.whl'), None)
+ if included_pyhidra is None:
+ print('Warning: included pyhidra wheel was not found', file=sys.stderr)
+ return
+ included_version: Version = Version(included_pyhidra.name.split('-')[1])
+ current_version: Version = Version(current_pyhidra_version)
+ if included_version > current_version:
+ choice: str = input(f'Do you wish to upgrade Pyhidra {current_version} to {included_version} (y/n)? ')
+ if choice.lower() in ('y', 'yes'):
+ pip_args.append('-U')
+ subprocess.check_call(pip_args)
+ return True
+ else:
+ print('Skipping upgrade')
+ return False
+
+def install(pip_args: List[str], dist_dir: Path) -> bool:
+ choice: str = input('Do you wish to install Pyhidra (y/n)? ')
+ if choice.lower() in ('y', 'yes'):
+ subprocess.check_call(pip_args)
+ return True
+ elif choice.lower() in ('n', 'no'):
+ return False
+ else:
+ print('Please answer yes or no.')
+ return False
+
+def main() -> None:
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(prog=Path(__file__).name)
+ parser.add_argument('install_dir', metavar='', help='Ghidra installation directory')
+ parser.add_argument('-c', '--console', action='store_true', help='Force console launch')
+ parser.add_argument('-d', '--dev', action='store_true', help='Ghidra development mode')
+ parser.add_argument('-H', '--headless', action='store_true', help='Ghidra headless mode')
+ args, remaining = parser.parse_known_args()
+
+ # Setup variables
+ python_cmd: str = sys.executable
+ install_dir: Path = Path(args.install_dir)
+ venv_dir: Path = install_dir / 'build' / 'venv'
+ pyhidra_dir: Path = install_dir / 'Ghidra' / 'Features' / 'Pyhidra'
+ src_dir: Path = pyhidra_dir / 'src' / 'main' / 'py'
+ dist_dir: Path = pyhidra_dir / 'pypkg' / 'dist'
+
+ # If headless, force console mode
+ if args.headless:
+ args.console = True
+
+ if args.dev:
+ # If in dev mode, launch pyhidra from the source tree using the development virtual environment
+ if not venv_dir.is_dir():
+ print('Virtual environment not found!')
+ print('Run "gradle prepdev" and try again.')
+ return
+ win_python_cmd = str(venv_dir / 'Scripts' / 'python.exe')
+ linux_python_cmd = str(venv_dir / 'bin' / 'python3')
+ python_cmd = win_python_cmd if os.name == 'nt' else linux_python_cmd
+ else:
+ # If in release mode, offer to install or upgrade pyhidra before launching from user-controlled environment
+ pip_args: List[str] = [python_cmd, '-m', 'pip', 'install', '--no-index', '-f', str(dist_dir), 'pyhidra']
+ try:
+ import pyhidra
+ upgrade(pip_args, dist_dir, pyhidra.__version__)
+ except ImportError:
+ if not install(pip_args, dist_dir):
+ return
+
+ # Launch Pyhidra
+ py_args: List[str] = [python_cmd, '-m', 'pyhidra.ghidra_launch', '--install-dir', str(install_dir)]
+ if args.headless:
+ py_args += ['ghidra.app.util.headless.AnalyzeHeadless']
+ else:
+ py_args += ['-g', 'ghidra.GhidraRun']
+ if args.console:
+ subprocess.call(py_args + remaining)
+ else:
+ creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
+ subprocess.Popen(py_args + remaining, creationflags=creation_flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+if __name__ == "__main__":
+ main()
diff --git a/Ghidra/Features/Pyhidra/src/main/help/help/TOC_Source.xml b/Ghidra/Features/Pyhidra/src/main/help/help/TOC_Source.xml
new file mode 100644
index 0000000000..ffda807b49
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/help/help/TOC_Source.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/Ghidra/Features/Pyhidra/src/main/help/help/topics/Pyhidra/interpreter.html b/Ghidra/Features/Pyhidra/src/main/help/help/topics/Pyhidra/interpreter.html
new file mode 100644
index 0000000000..1c57688c86
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/help/help/topics/Pyhidra/interpreter.html
@@ -0,0 +1,167 @@
+
+
+
+
+ Pyhidra Interpreter
+
+
+
+
+ Pyhidra Interpreter
+
+
+ The Ghidra Pyhidra Interpreter provides a full general-purpose Python interactive shell
+ and allows you to interact with your current Ghidra session by exposing Ghidra's powerful Java
+ API through the magic of Jpype.
+
+
+ Environment
+
+
+ The Ghidra Pyhidra Interpreter is configured to run in a similar context as a Ghidra
+ script. Therefore, you immediately have access to variables such as currentProgram ,
+ currentSelection , currentAddress , etc without needing to import them.
+ These variables exist as Java objects behind the scenes, but Jpype allows you to interact with
+ them through a Python interface, which is similar to Java in some ways.
+
+
+
+ As in Java, classes outside of your current package/module need to be explicitly imported.
+ For example, consider the following code snippet:
+
+
+
+
+ # Get a data type from the user
+ tool = state.getTool()
+ dtm = currentProgram.getDataTypeManager()
+ from ghidra.app.util.datatype import DataTypeSelectionDialog
+ from ghidra.util.data.DataTypeParser import AllowedDataTypes
+ selectionDialog = DataTypeSelectionDialog(tool, dtm, -1, AllowedDataTypes.FIXED_LENGTH)
+ tool.showDialog(selectionDialog)
+ dataType = selectionDialog.getUserChosenDataType()
+ if dataType != None: print("Chosen data type: " + str(dataType))
+
+
+
+ currentProgram and state are defined within the Ghidra scripting class
+ hierarchy, so nothing has to be explicitly imported before they can be used. However, because
+ the DataTypeSelectionDialog class and AllowedDataType enum reside in
+ different packages, they must be explicitly imported. Failure to do so will result in a
+ Python NameError .
+
+
+
+ Clear
+
+
+ This command clears the interpreter's display. Its effect is purely visual.
+ It does not affect the state of the interpreter in any way.
+
+
+
+ Interrupt
+
+
+ This command issues a keyboard interrupt to the interpreter, which can be used to interrupt
+ long running commands or loops.
+
+
+
+ Reset
+
+
+ This command resets the interpreter, which clears the display and resets all state.
+
+
+
+ Keybindings
+
+
+ The Ghidra Pyhidra Interpreter supports the following hard-coded keybindings:
+
+ (up): Move backward in command stack
+ (down): Move forward in command stack
+ TAB: Show code completion window
+
+
+
+ With the code completion window open:
+
+ TAB: Insert currently-selected code completion (if no completion selected, select the first available)
+ ENTER: Insert selected completion (if any) and close the completion window
+ (up): Select previous code completion
+ (down): Select next code completion
+ ESC: Hide code completion window
+
+
+
+
+ initializer = null;
+
+ public final InterpreterGhidraScript script = new InterpreterGhidraScript();
+ public PyhidraInterpreter interpreter;
+
+ public PyhidraPlugin(PluginTool tool) {
+ super(tool);
+ GhidraState state = new GhidraState(tool, tool.getProject(), null, null, null, null);
+ // use the copy constructor so this state doesn't fire plugin events
+ script.set(new GhidraState(state), null, null);
+ }
+
+ /**
+ * Sets the plugin's Python side initializer.
+ *
+ * This method is for internal use only and is only public so it can be
+ * called from Python.
+ *
+ * @param initializer the Python side initializer
+ * @throws AssertException if the code completer has already been set
+ */
+ public static void setInitializer(Consumer initializer) {
+ if (PyhidraPlugin.initializer != null) {
+ throw new AssertException("PyhidraPlugin initializer has already been set");
+ }
+ PyhidraPlugin.initializer = initializer;
+ }
+
+ @Override
+ public void init() {
+ interpreter = new PyhidraInterpreter(this, PyhidraPlugin.initializer != null);
+ if (initializer != null) {
+ initializer.accept(this);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ interpreter.dispose();
+ super.dispose();
+ }
+
+ @Override
+ protected void programActivated(Program program) {
+ script.setCurrentProgram(program);
+ }
+
+ @Override
+ protected void programDeactivated(Program program) {
+ if (script.getCurrentProgram() == program) {
+ script.setCurrentProgram(null);
+ }
+ }
+
+ @Override
+ protected void locationChanged(ProgramLocation location) {
+ script.setCurrentLocation(location);
+ }
+
+ @Override
+ protected void selectionChanged(ProgramSelection selection) {
+ script.setCurrentSelection(selection);
+ }
+
+ @Override
+ protected void highlightChanged(ProgramSelection highlight) {
+ script.setCurrentHighlight(highlight);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PyhidraScriptProvider.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PyhidraScriptProvider.java
new file mode 100644
index 0000000000..8ba0669e54
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PyhidraScriptProvider.java
@@ -0,0 +1,133 @@
+package ghidra.pyhidra;
+
+import java.io.*;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.function.Consumer;
+
+import generic.jar.ResourceFile;
+import ghidra.app.script.*;
+import ghidra.app.util.headless.HeadlessScript;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.listing.Program;
+import ghidra.program.util.ProgramLocation;
+import ghidra.program.util.ProgramSelection;
+import ghidra.pyhidra.PythonFieldExposer.ExposedFields;
+import ghidra.util.exception.AssertException;
+import ghidra.util.SystemUtilities;
+import ghidra.util.task.TaskMonitor;
+
+/**
+ * {@link GhidraScript} provider for native python3 scripts
+ */
+public final class PyhidraScriptProvider extends AbstractPythonScriptProvider {
+
+ private static Consumer scriptRunner = null;
+
+ /**
+ * Sets the Python side script runner.
+ *
+ * This method is for internal use only and is only public so it can be
+ * called from Python.
+ *
+ * @param scriptRunner the Python side script runner
+ * @throws AssertException if the script runner has already been set
+ */
+ public static void setScriptRunner(Consumer scriptRunner) {
+ if (PyhidraScriptProvider.scriptRunner != null) {
+ throw new AssertException("scriptRunner has already been set");
+ }
+ PyhidraScriptProvider.scriptRunner = scriptRunner;
+ }
+
+ @Override
+ public String getDescription() {
+ return PyhidraPlugin.TITLE;
+ }
+
+ @Override
+ public String getRuntimeEnvironmentName() {
+ return PyhidraPlugin.TITLE;
+ }
+
+ @Override
+ public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer)
+ throws GhidraScriptLoadException {
+ if (scriptRunner == null) {
+ String msg = "Ghidra was not started with pyhidra. Python is not available";
+ throw new GhidraScriptLoadException(msg);
+ }
+ GhidraScript script = SystemUtilities.isInHeadlessMode() ? new PyhidraHeadlessScript()
+ : new PyhidraGhidraScript();
+ script.setSourceFile(sourceFile);
+ return script;
+ }
+
+ @ExposedFields(
+ exposer = PyhidraGhidraScript.ExposedField.class,
+ names = {
+ "currentAddress", "currentLocation", "currentSelection",
+ "currentHighlight", "currentProgram", "monitor",
+ "potentialPropertiesFileLocs", "propertiesFileParams",
+ "sourceFile", "state", "writer"
+ },
+ types = {
+ Address.class, ProgramLocation.class, ProgramSelection.class,
+ ProgramSelection.class, Program.class, TaskMonitor.class,
+ List.class, GhidraScriptProperties.class,
+ ResourceFile.class, GhidraState.class, PrintWriter.class
+ }
+ )
+ final static class PyhidraGhidraScript extends GhidraScript
+ implements PythonFieldExposer {
+
+ @Override
+ public void run() {
+ scriptRunner.accept(this);
+ }
+
+ /**
+ * Helper inner class that can create a {@link MethodHandles.Lookup}
+ * that can access the protected fields of the {@link GhidraScript}
+ */
+ private static class ExposedField extends PythonFieldExposer.ExposedField {
+ public ExposedField(String name, Class> type) {
+ super(MethodHandles.lookup().in(PyhidraGhidraScript.class), name, type);
+ }
+ }
+ }
+
+ @ExposedFields(
+ exposer = PyhidraHeadlessScript.ExposedField.class,
+ names = {
+ "currentAddress", "currentLocation", "currentSelection",
+ "currentHighlight", "currentProgram", "monitor",
+ "potentialPropertiesFileLocs", "propertiesFileParams",
+ "sourceFile", "state", "writer"
+ },
+ types = {
+ Address.class, ProgramLocation.class, ProgramSelection.class,
+ ProgramSelection.class, Program.class, TaskMonitor.class,
+ List.class, GhidraScriptProperties.class,
+ ResourceFile.class, GhidraState.class, PrintWriter.class
+ }
+ )
+ final static class PyhidraHeadlessScript extends HeadlessScript
+ implements PythonFieldExposer {
+
+ @Override
+ public void run() {
+ scriptRunner.accept(this);
+ }
+
+ /**
+ * Helper inner class that can create a {@link MethodHandles.Lookup}
+ * that can access the protected fields of the {@link GhidraScript}
+ */
+ private static class ExposedField extends PythonFieldExposer.ExposedField {
+ public ExposedField(String name, Class> type) {
+ super(MethodHandles.lookup().in(PyhidraHeadlessScript.class), name, type);
+ }
+ }
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PythonFieldExposer.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PythonFieldExposer.java
new file mode 100644
index 0000000000..945bf97706
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/PythonFieldExposer.java
@@ -0,0 +1,144 @@
+package ghidra.pyhidra;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.invoke.ConstantBootstraps;
+import java.lang.invoke.VarHandle;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Constructor;
+import java.util.Map;
+
+import ghidra.util.Msg;
+import ghidra.util.exception.AssertException;
+
+/**
+ * A marker interface to apply Jpype class customizations to a class.
+ *
+ * The Jpype class customizations will create Python properties which can access protected fields.
+ *
+ * This interface is for internal use only and is only public so it can be
+ * visible to Python to apply the Jpype class customizations.
+ */
+public sealed interface PythonFieldExposer permits PyhidraScriptProvider.PyhidraGhidraScript,
+ PyhidraScriptProvider.PyhidraHeadlessScript {
+
+ /**
+ * Gets a mapping of all the explicitly exposed fields of a class.
+ *
+ * This method is for internal use only and is only public so it can be
+ * called from Python.
+ *
+ * @param cls the PythonFieldExposer class
+ * @return a map of the exposed fields
+ */
+ public static Map getProperties(
+ Class extends PythonFieldExposer> cls) {
+ try {
+ return doGetProperties(cls);
+ }
+ catch (Throwable t) {
+ Msg.error(PythonFieldExposer.class,
+ "Failed to expose fields for " + cls.getSimpleName(), t);
+ return Map.of();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map doGetProperties(
+ Class extends PythonFieldExposer> cls)
+ throws Throwable {
+ ExposedFields fields = cls.getAnnotation(ExposedFields.class);
+ String[] names = fields.names();
+ Class>[] types = fields.types();
+ if (names.length != types.length) {
+ throw new AssertException("Improperly applied ExposedFields on " + cls.getSimpleName());
+ }
+
+ Constructor extends ExposedField> c =
+ fields.exposer().getConstructor(String.class, Class.class);
+ Map.Entry[] properties = new Map.Entry[names.length];
+ for (int i = 0; i < names.length; i++) {
+ properties[i] = Map.entry(names[i], c.newInstance(names[i], types[i]));
+ }
+ return Map.ofEntries(properties);
+ }
+
+ /**
+ * An annotation for exposing protected fields of a class to Python
+ */
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.RUNTIME)
+ static @interface ExposedFields {
+ /**
+ * @return the {@link ExposedField} subclass with access to the protected fields
+ */
+ public Class extends ExposedField> exposer();
+
+ /**
+ * @return the names of the protected fields to be exposed
+ */
+ public String[] names();
+
+ /**
+ * @return the types of the protected fields to be exposed
+ */
+ public Class>[] types();
+ }
+
+ /**
+ * Base class for making a protected field accessible from Python.
+ *
+ * Child classes are to be defined inside the class containing the fields to be exposed.
+ * The only requirement of the child class is to provide a {@link Lookup} with access
+ * to the protected fields, to the {@link ExposedField} constructor as shown below.
+ *
+ * {@snippet lang="java" :
+ * public class ExampleClass implements PythonFieldExposer {
+ * protected int counter = 0;
+ *
+ * private static class ExposedField extends PythonFieldExposer.ExposedField {
+ * public ExposedField(String name, Class> type) {
+ * super(MethodHandles.lookup().in(ExampleClass.class), name, type);
+ * }
+ * }
+ * }
+ * }
+ */
+ static abstract class ExposedField {
+ private final VarHandle handle;
+
+ /**
+ * Constructs a new {@link ExposedField}
+ *
+ * @param lookup the {@link Lookup} with access to the protected field
+ * @param name the name of the protected field
+ * @param type the type of the protected field
+ */
+ protected ExposedField(Lookup lookup, String name, Class> type) {
+ handle = ConstantBootstraps.fieldVarHandle(lookup, name, VarHandle.class,
+ lookup.lookupClass(), type);
+ }
+
+ /**
+ * Gets the field value
+ *
+ * @param self the instance containing the field
+ * @return the field value
+ */
+ public final Object fget(Object self) {
+ return handle.get(self);
+ }
+
+ /**
+ * Sets the field value
+ *
+ * @param self the instance containing the field
+ * @param value the field value
+ */
+ public final void fset(Object self, Object value) {
+ handle.set(self, value);
+ }
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/CancelAction.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/CancelAction.java
new file mode 100644
index 0000000000..6e9f672863
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/CancelAction.java
@@ -0,0 +1,36 @@
+package ghidra.pyhidra.interpreter;
+
+import java.awt.event.KeyEvent;
+import javax.swing.ImageIcon;
+
+import ghidra.pyhidra.PyhidraPlugin;
+import docking.ActionContext;
+import docking.action.KeyBindingData;
+import docking.action.DockingAction;
+import docking.action.ToolBarData;
+import ghidra.util.HelpLocation;
+import resources.ResourceManager;
+
+import static docking.DockingUtils.CONTROL_KEY_MODIFIER_MASK;
+
+final class CancelAction extends DockingAction {
+
+ private final PyhidraConsole console;
+
+ CancelAction(PyhidraConsole console) {
+ super("Cancel", PyhidraPlugin.class.getSimpleName());
+ this.console = console;
+ setDescription("Interrupt the interpreter");
+ ImageIcon image = ResourceManager.loadImage("images/dialog-cancel.png");
+ setToolBarData(new ToolBarData(image));
+ setEnabled(true);
+ KeyBindingData key = new KeyBindingData(KeyEvent.VK_I, CONTROL_KEY_MODIFIER_MASK);
+ setKeyBindingData(key);
+ setHelpLocation(new HelpLocation(PyhidraPlugin.TITLE, "Interrupt_Interpreter"));
+ }
+
+ @Override
+ public void actionPerformed(ActionContext context) {
+ console.interrupt();
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterGhidraScript.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterGhidraScript.java
new file mode 100644
index 0000000000..043db5534a
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterGhidraScript.java
@@ -0,0 +1,76 @@
+package ghidra.pyhidra.interpreter;
+
+import java.io.PrintWriter;
+
+import ghidra.app.script.GhidraScript;
+import ghidra.app.script.GhidraState;
+import ghidra.program.model.address.Address;
+import ghidra.program.model.listing.Program;
+import ghidra.program.util.ProgramLocation;
+import ghidra.program.util.ProgramSelection;
+
+/**
+ * Custom {@link GhidraScript} only for use with the pyhidra interpreter console
+ */
+public final class InterpreterGhidraScript extends GhidraScript {
+
+ // public default constructor for use by PyhidraPlugin
+ // the default constructor for FlatProgramAPI has protected visibility
+ public InterpreterGhidraScript() {
+ }
+
+ @Override
+ public void run() {
+ // we run in the interpreter console so we do nothing here
+ }
+
+ public Address getCurrentAddress() {
+ return currentAddress;
+ }
+
+ public ProgramLocation getCurrentLocation() {
+ return currentLocation;
+ }
+
+ public ProgramSelection getCurrentSelection() {
+ return currentSelection;
+ }
+
+ public ProgramSelection getCurrentHighlight() {
+ return currentHighlight;
+ }
+
+ public PrintWriter getWriter() {
+ return writer;
+ }
+
+ public void setCurrentProgram(Program program) {
+ currentProgram = program;
+ state.setCurrentProgram(program);
+ }
+
+ public void setCurrentAddress(Address address) {
+ currentAddress = address;
+ state.setCurrentAddress(address);
+ }
+
+ public void setCurrentLocation(ProgramLocation location) {
+ currentLocation = location;
+ currentAddress = location != null ? location.getAddress() : null;
+ state.setCurrentLocation(location);
+ }
+
+ public void setCurrentSelection(ProgramSelection selection) {
+ currentSelection = selection;
+ state.setCurrentSelection(selection);
+ }
+
+ public void setCurrentHighlight(ProgramSelection highlight) {
+ currentHighlight = highlight;
+ state.setCurrentHighlight(highlight);
+ }
+
+ public void set(GhidraState state, PrintWriter writer) {
+ set(state, new InterpreterTaskMonitor(writer), writer);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterTaskMonitor.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterTaskMonitor.java
new file mode 100644
index 0000000000..ff432d7905
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/InterpreterTaskMonitor.java
@@ -0,0 +1,19 @@
+package ghidra.pyhidra.interpreter;
+
+import java.io.PrintWriter;
+
+import ghidra.util.task.TaskMonitorAdapter;
+
+final class InterpreterTaskMonitor extends TaskMonitorAdapter {
+
+ private PrintWriter output = null;
+
+ InterpreterTaskMonitor(PrintWriter stdOut) {
+ output = stdOut;
+ }
+
+ @Override
+ public void setMessage(String message) {
+ output.println(": " + message);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraConsole.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraConsole.java
new file mode 100644
index 0000000000..37944dd669
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraConsole.java
@@ -0,0 +1,37 @@
+package ghidra.pyhidra.interpreter;
+
+import java.util.List;
+
+import ghidra.app.plugin.core.console.CodeCompletion;
+import ghidra.app.plugin.core.interpreter.InterpreterConnection;
+import ghidra.util.Disposable;
+
+/**
+ * Console interface providing only the methods which need to be implemented in Python.
+ *
+ * This interface is for internal use only and is only public so it can be
+ * implemented in Python.
+ */
+public interface PyhidraConsole extends Disposable {
+
+ /**
+ * Generates code completions for the pyhidra interpreter
+ *
+ * @param cmd The command to get code completions for
+ * @param caretPos The position of the caret in the input string 'cmd'.
+ * It should satisfy the constraint {@literal "0 <= caretPos <= cmd.length()"}
+ * @return A {@link List} of {@link CodeCompletion code completions} for the given command
+ * @see InterpreterConnection InterpreterConnection.getCompletions(String, int)
+ */
+ List getCompletions(String cmd, int caretPos);
+
+ /**
+ * Restarts the pyhidra console
+ */
+ void restart();
+
+ /**
+ * Interrupts the code running in the pyhidra console
+ */
+ void interrupt();
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraInterpreter.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraInterpreter.java
new file mode 100644
index 0000000000..b85f5a41db
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/PyhidraInterpreter.java
@@ -0,0 +1,88 @@
+package ghidra.pyhidra.interpreter;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.swing.Icon;
+import ghidra.app.plugin.core.console.CodeCompletion;
+import ghidra.app.plugin.core.interpreter.InterpreterConnection;
+import ghidra.app.plugin.core.interpreter.InterpreterConsole;
+import ghidra.app.plugin.core.interpreter.InterpreterPanelService;
+import ghidra.pyhidra.PyhidraPlugin;
+import ghidra.util.Disposable;
+import ghidra.util.exception.AssertException;
+import resources.ResourceManager;
+
+/**
+ * The pyhidra interpreter connection
+ */
+public final class PyhidraInterpreter implements Disposable, InterpreterConnection {
+
+ private PyhidraConsole pyhidraConsole = null;
+ public final InterpreterConsole console;
+
+ public PyhidraInterpreter(PyhidraPlugin plugin, boolean isPythonAvailable) {
+ InterpreterPanelService service =
+ plugin.getTool().getService(InterpreterPanelService.class);
+ console = service.createInterpreterPanel(this, false);
+ if (!isPythonAvailable) {
+ console.addFirstActivationCallback(this::unavailableCallback);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ if (pyhidraConsole != null) {
+ pyhidraConsole.dispose();
+ }
+ console.dispose();
+ }
+
+ @Override
+ public Icon getIcon() {
+ return ResourceManager.loadImage("images/python.png");
+ }
+
+ @Override
+ public String getTitle() {
+ return PyhidraPlugin.TITLE;
+ }
+
+ @Override
+ public List getCompletions(String cmd) {
+ throw new AssertException("Unreachable, unimplemented and deprecated method");
+ }
+
+ @Override
+ public List getCompletions(String cmd, int caretPos) {
+ if (pyhidraConsole == null) {
+ return List.of();
+ }
+ return pyhidraConsole.getCompletions(cmd, caretPos);
+ }
+
+ private void unavailableCallback() {
+ console.setInputPermitted(false);
+ PrintWriter out = console.getOutWriter();
+ out.println("Ghidra was not started with pyhidra. Python is not available.");
+ }
+
+ /**
+ * Initializes the interpreter with the provided PyhidraConsole.
+ *
+ * This method is for internal use only and is only public so it can be
+ * called from Python.
+ *
+ * @param pythonSideConsole the python side console
+ * @throws AssertException if the interpreter has already been initialized
+ */
+ public void init(PyhidraConsole pythonSideConsole) {
+ if (pyhidraConsole != null) {
+ throw new AssertException("the interpreter has already been initialized");
+ }
+ pyhidraConsole = pythonSideConsole;
+ console.addFirstActivationCallback(pyhidraConsole::restart);
+ console.addAction(new CancelAction(pyhidraConsole));
+ console.addAction(new ResetAction(pyhidraConsole));
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/ResetAction.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/ResetAction.java
new file mode 100644
index 0000000000..1de493a3d0
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/interpreter/ResetAction.java
@@ -0,0 +1,36 @@
+package ghidra.pyhidra.interpreter;
+
+import java.awt.event.KeyEvent;
+import javax.swing.ImageIcon;
+
+import ghidra.pyhidra.PyhidraPlugin;
+import ghidra.util.HelpLocation;
+import docking.ActionContext;
+import docking.action.DockingAction;
+import docking.action.KeyBindingData;
+import docking.action.ToolBarData;
+import resources.ResourceManager;
+
+import static docking.DockingUtils.CONTROL_KEY_MODIFIER_MASK;
+
+final class ResetAction extends DockingAction {
+
+ private final PyhidraConsole console;
+
+ ResetAction(PyhidraConsole console) {
+ super("Reset", PyhidraPlugin.class.getSimpleName());
+ this.console = console;
+ setDescription("Reset the interpreter");
+ ImageIcon image = ResourceManager.loadImage("images/reload3.png");
+ setToolBarData(new ToolBarData(image));
+ setEnabled(true);
+ KeyBindingData key = new KeyBindingData(KeyEvent.VK_D, CONTROL_KEY_MODIFIER_MASK);
+ setKeyBindingData(key);
+ setHelpLocation(new HelpLocation(PyhidraPlugin.TITLE, "Reset_Interpreter"));
+ }
+
+ @Override
+ public void actionPerformed(ActionContext context) {
+ console.restart();
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/AbstractJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/AbstractJavaProperty.java
new file mode 100644
index 0000000000..e9cb3cf7cd
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/AbstractJavaProperty.java
@@ -0,0 +1,83 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * Abstract base class for implementing a {@link JavaProperty}.
+ *
+ * This class provides the fset implementation as well as all helpers so
+ * that each child class only needs to define a constructor and a fget
+ * method returning the correct primitive type. Each child class can
+ * implement fget as follows:
+ *
+ * {@snippet lang="java" :
+ * public type fget(Object self) throws Throwable { // @highlight substring="type"
+ * return doGet(self);
+ * }
+ * }
+ *
+ * The pyhidra internals expects every {@link JavaProperty} to be an instance of this class.
+ * No checking is required or performed since the {@link JavaProperty} interface and this
+ * class are sealed.
+ */
+abstract sealed class AbstractJavaProperty implements JavaProperty permits
+ BooleanJavaProperty, ByteJavaProperty, CharacterJavaProperty,
+ DoubleJavaProperty, FloatJavaProperty, IntegerJavaProperty,
+ LongJavaProperty, ObjectJavaProperty, ShortJavaProperty {
+
+ /**
+ * The name of the property
+ */
+ public final String field;
+
+ // The handles to the underlying get/set methods
+ private final MethodHandle getter;
+ private final MethodHandle setter;
+
+ protected AbstractJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ this.field = field;
+ this.getter = getter;
+ this.setter = setter;
+ }
+
+ /**
+ * Checks if this property has a getter
+ *
+ * @return true if this property has a getter
+ */
+ public boolean hasGetter() {
+ return getter != null;
+ }
+
+ /**
+ * Checks if this property has a setter
+ *
+ * @return true if this property has a setter
+ */
+ public boolean hasSetter() {
+ return setter != null;
+ }
+
+ // this is only for testing
+ boolean hasValidSetter() {
+ if (setter == null) {
+ return false;
+ }
+ if (getter == null) {
+ return true;
+ }
+ Class> getterType = PropertyUtils.boxPrimitive(getter.type().returnType());
+ // for a MethodType the parameter we want is at index 1
+ Class> setterType = PropertyUtils.boxPrimitive(setter.type().parameterType(1));
+ return getterType == setterType;
+ }
+
+ protected final T doGet(Object self) throws Throwable {
+ return (T) getter.invoke(self);
+ }
+
+ @Override
+ public final void fset(Object self, T value) throws Throwable {
+ setter.invoke(self, value);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/BooleanJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/BooleanJavaProperty.java
new file mode 100644
index 0000000000..7e764b3f2e
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/BooleanJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive boolean type
+ */
+public final class BooleanJavaProperty extends AbstractJavaProperty {
+
+ BooleanJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public boolean fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ByteJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ByteJavaProperty.java
new file mode 100644
index 0000000000..4afa658564
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ByteJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive byte type
+ */
+public final class ByteJavaProperty extends AbstractJavaProperty {
+
+ ByteJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public byte fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/CharacterJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/CharacterJavaProperty.java
new file mode 100644
index 0000000000..fbbb5fdafb
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/CharacterJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive char type
+ */
+public final class CharacterJavaProperty extends AbstractJavaProperty {
+
+ CharacterJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public char fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/DoubleJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/DoubleJavaProperty.java
new file mode 100644
index 0000000000..8b75ffb5d7
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/DoubleJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive double type
+ */
+public final class DoubleJavaProperty extends AbstractJavaProperty {
+
+ DoubleJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public double fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/FloatJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/FloatJavaProperty.java
new file mode 100644
index 0000000000..88bbb340af
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/FloatJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive float type
+ */
+public final class FloatJavaProperty extends AbstractJavaProperty {
+
+ FloatJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public float fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/IntegerJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/IntegerJavaProperty.java
new file mode 100644
index 0000000000..c14ff1ce77
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/IntegerJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive int type
+ */
+public final class IntegerJavaProperty extends AbstractJavaProperty {
+
+ IntegerJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public int fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaProperty.java
new file mode 100644
index 0000000000..08d1fdfd1e
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaProperty.java
@@ -0,0 +1,28 @@
+package ghidra.pyhidra.property;
+
+/**
+ * Property interface for creating a Python property for getters and setters.
+ *
+ * Each implementation is required to have a defined fget method which returns
+ * the corresponding primitive type. By doing so we can utilize Python duck typing,
+ * auto boxing/unboxing and the Jpype conversion system to automatically convert
+ * the primitive return types to the equivalent Python type. This removes the
+ * headache of having to carefully and explicitly cast things to an int to
+ * avoid exceptions in Python code related to type conversion or type attributes.
+ *
+ * The fget and fset methods are named to correspond with the fget and fset members
+ * of Python's property type.
+ */
+public sealed interface JavaProperty permits AbstractJavaProperty {
+
+ /**
+ * The method to be used as the fset value for a Python property.
+ *
+ * This method will be called by the Python property __set__ function.
+ *
+ * @param self the object containing the property
+ * @param value the value to be set
+ * @throws Throwable if any exception occurs while setting the value
+ */
+ public abstract void fset(Object self, T value) throws Throwable;
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaPropertyFactory.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaPropertyFactory.java
new file mode 100644
index 0000000000..11a6278c22
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/JavaPropertyFactory.java
@@ -0,0 +1,48 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * Factory class for a {@link JavaProperty}
+ */
+class JavaPropertyFactory {
+
+ private JavaPropertyFactory() {
+ }
+
+ static JavaProperty> getProperty(String field, MethodHandle getter, MethodHandle setter) {
+ Class> cls =
+ getter != null ? getter.type().returnType() : setter.type().lastParameterType();
+ if (!cls.isPrimitive()) {
+ return new ObjectJavaProperty(field, getter, setter);
+ }
+ if (cls == Boolean.TYPE) {
+ return new BooleanJavaProperty(field, getter, setter);
+ }
+ if (cls == Byte.TYPE) {
+ return new ByteJavaProperty(field, getter, setter);
+ }
+ if (cls == Character.TYPE) {
+ return new CharacterJavaProperty(field, getter, setter);
+ }
+ if (cls == Double.TYPE) {
+ return new DoubleJavaProperty(field, getter, setter);
+ }
+ if (cls == Float.TYPE) {
+ return new FloatJavaProperty(field, getter, setter);
+ }
+ if (cls == Integer.TYPE) {
+ return new IntegerJavaProperty(field, getter, setter);
+ }
+ if (cls == Long.TYPE) {
+ return new LongJavaProperty(field, getter, setter);
+ }
+ if (cls == Short.TYPE) {
+ return new ShortJavaProperty(field, getter, setter);
+ }
+ // it's better than nothing at all
+ // users will just need to be extra careful about casting to whatever the new primitive
+ // type is when using a getter/setter
+ return new ObjectJavaProperty(field, getter, setter);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/LongJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/LongJavaProperty.java
new file mode 100644
index 0000000000..9bda93d281
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/LongJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive long type
+ */
+public final class LongJavaProperty extends AbstractJavaProperty {
+
+ LongJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public long fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ObjectJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ObjectJavaProperty.java
new file mode 100644
index 0000000000..ea62fc38ab
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ObjectJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for a reference type
+ */
+public final class ObjectJavaProperty extends AbstractJavaProperty {
+
+ ObjectJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public Object fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/PropertyUtils.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/PropertyUtils.java
new file mode 100644
index 0000000000..d62317855f
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/PropertyUtils.java
@@ -0,0 +1,269 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import ghidra.util.Msg;
+
+/**
+ * Utility class for working with classes to obtain and create Python properties.
+ *
+ * This class is for internal use only and is only public so it can be
+ * reached from Python.
+ */
+public class PropertyUtils {
+
+ private PropertyUtils() {
+ }
+
+ /**
+ * Gets the boxed class for a primitive type
+ *
+ * @param cls the primitive class type
+ * @return the boxed class for a primitive type or the original class if not a primitive type
+ */
+ static Class> boxPrimitive(Class> cls) {
+ if (!cls.isPrimitive()) {
+ return cls;
+ }
+ // sure there are cleaner ways to do this
+ // you could do a switch over the first character from Class.descriptorString
+ // however, for a primitive class, descriptorString goes through exactly this
+ // just to produce the descriptor string so there is really no point
+ if (cls == Boolean.TYPE) {
+ return Boolean.class;
+ }
+ if (cls == Byte.TYPE) {
+ return Byte.class;
+ }
+ if (cls == Character.TYPE) {
+ return Character.class;
+ }
+ if (cls == Double.TYPE) {
+ return Double.class;
+ }
+ if (cls == Float.TYPE) {
+ return Float.class;
+ }
+ if (cls == Integer.TYPE) {
+ return Integer.class;
+ }
+ if (cls == Long.TYPE) {
+ return Long.class;
+ }
+ if (cls == Short.TYPE) {
+ return Short.class;
+ }
+ // this allows us to still give a functional property
+ // if a new primitive type is ever added it can still work
+ return cls;
+ }
+
+ /**
+ * Gets an array of {@link JavaProperty} for the provided class.
+ *
+ * This method is for internal use only and is only public
+ * so it can be called from Python.
+ *
+ * @param cls the class to get the properties for
+ * @return an array of properties
+ */
+ public static JavaProperty>[] getProperties(Class> cls) {
+ if (cls == Object.class) {
+ return new JavaProperty[0];
+ }
+ try {
+ return doGetProperties(cls);
+ }
+ catch (Throwable t) {
+ Msg.error(PropertyUtils.class,
+ "Failed to extract properties for " + cls.getSimpleName(), t);
+ return new JavaProperty>[0];
+ }
+ }
+
+ private static JavaProperty>[] doGetProperties(Class> cls) throws Throwable {
+ PropertyPairFactory factory;
+ try {
+ factory = new PropertyPairFactory(cls);
+ }
+ catch (IllegalArgumentException e) {
+ // skip illegal lookup class
+ return new JavaProperty>[0];
+ }
+ return getMethods(cls)
+ .filter(PropertyUtils::methodFilter)
+ .map(PropertyUtils::toProperty)
+ .collect(Collectors.groupingBy(PartialProperty::getName))
+ .values()
+ .stream()
+ .map(factory::merge)
+ .flatMap(Optional::stream)
+ .toArray(JavaProperty>[]::new);
+ }
+
+ private static Stream getMethods(Class> cls) {
+ // customizations added using JClass._customize are inherited
+ // therfore we only care about the ones declared by this class
+ return Arrays.stream(cls.getDeclaredMethods())
+ .filter(PropertyUtils::methodFilter);
+ }
+
+ private static boolean methodFilter(Method m) {
+ /*
+ This is much simpler than it looks.
+
+ A method is considered a getter/setter if it meets the following:
+
+ 1. Has public visibility and is not static.
+ 2. Has a name starting with lowercase get/set/is with the character after
+ the prefix being uppercase.
+ 3. A getter has 0 parameters and a non-void return type.
+ A setter has 1 parameter and must not return anything.
+ An is getter must return a boolean or Boolean.
+ 4. The method name must be longer than the prefix.
+
+ The first few checks are done to short circuit and return false sooner rather than later.
+ */
+
+ if (!isPublic(m)) {
+ return false;
+ }
+
+ int paramCount = m.getParameterCount();
+ if (paramCount > 1) {
+ return false;
+ }
+
+ Class> resultType = m.getReturnType();
+ String name = m.getName();
+ int nameLength = name.length();
+ if (nameLength < 3) {
+ return false;
+ }
+ switch (name.charAt(0)) {
+ case 'g':
+ if (paramCount == 0 && resultType != Void.TYPE) {
+ if (nameLength > 3 && name.startsWith("get")) {
+ return Character.isUpperCase(name.charAt(3));
+ }
+ }
+ return false;
+ case 'i':
+ if (paramCount == 0 &&
+ (resultType == Boolean.TYPE || resultType == Boolean.class)) {
+ if (nameLength > 2 && name.startsWith("is")) {
+ return Character.isUpperCase(name.charAt(2));
+ }
+ }
+ return false;
+ case 's':
+ if (paramCount == 1 && resultType == Void.TYPE) {
+ if (nameLength > 3 && name.startsWith("set")) {
+ return Character.isUpperCase(name.charAt(3));
+ }
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ private static boolean isPublic(Method m) {
+ int mod = m.getModifiers();
+ return Modifier.isPublic(mod) && !Modifier.isStatic(mod);
+ }
+
+ /**
+ * Helper class for merging methods and removing a layer of reflection
+ */
+ private static class PropertyPairFactory {
+ private final Lookup lookup;
+
+ private PropertyPairFactory(Class> c) {
+ lookup = MethodHandles.publicLookup();
+ }
+
+ private Optional> merge(List pairs) {
+ try {
+ if (pairs.size() == 1) {
+ PartialProperty p = pairs.get(0);
+ MethodHandle h = lookup.unreflect(p.m);
+ JavaProperty> res =
+ p.isGetter() ? JavaPropertyFactory.getProperty(p.name, h, null)
+ : JavaPropertyFactory.getProperty(p.name, null, h);
+ return Optional.of(res);
+ }
+ PartialProperty g = pairs.stream()
+ .filter(PartialProperty::isGetter)
+ .findFirst()
+ .orElse(null);
+ if (g != null) {
+ // go through all remaining methods and take the first matching pair
+ // it does not matter if one is a boxed primitive and the other is
+ // unboxed because the JavaProperty will use the primitive type anyway
+ Class> target = boxPrimitive(g.m.getReturnType());
+ PartialProperty s = pairs.stream()
+ .filter(PartialProperty::isSetter)
+ .filter(p -> boxPrimitive(p.m.getParameterTypes()[0]) == target)
+ .findFirst()
+ .orElse(null);
+ MethodHandle gh = lookup.unreflect(g.m);
+ MethodHandle sh = s != null ? lookup.unreflect(s.m) : null;
+ return Optional.of(JavaPropertyFactory.getProperty(g.name, gh, sh));
+ }
+ }
+ catch (IllegalAccessException e) {
+ // this is a class in java.lang.invoke or java.lang.reflect
+ // the JVM doesn't allow the creation of handles for these
+ }
+ return Optional.empty();
+ }
+ }
+
+ private static PartialProperty toProperty(Method m) {
+ // all non properties have already been filtered out
+ String name = m.getName();
+ if (name.charAt(0) == 'i') {
+ name = name.substring(2);
+ }
+ else {
+ name = name.substring(3);
+ }
+ name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
+ return new PartialProperty(m, name);
+ }
+
+ /**
+ * Helper class for combining the methods into a property
+ */
+ private static class PartialProperty {
+ private final Method m;
+ private final String name;
+
+ private PartialProperty(Method m, String name) {
+ this.m = m;
+ this.name = name;
+ }
+
+ public boolean isGetter() {
+ return m.getParameterCount() == 0 && m.getReturnType() != Void.TYPE;
+ }
+
+ public boolean isSetter() {
+ return m.getParameterCount() == 1 && m.getReturnType() == Void.TYPE;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ShortJavaProperty.java b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ShortJavaProperty.java
new file mode 100644
index 0000000000..03f2b6b49a
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/java/ghidra/pyhidra/property/ShortJavaProperty.java
@@ -0,0 +1,26 @@
+package ghidra.pyhidra.property;
+
+import java.lang.invoke.MethodHandle;
+
+/**
+ * The {@link JavaProperty} for the primitive short type
+ */
+public final class ShortJavaProperty extends AbstractJavaProperty {
+
+ ShortJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
+ super(field, getter, setter);
+ }
+
+ /**
+ * The method to be used as the fget value for a Python property.
+ *
+ * This method will be called by the Python property __get__ function.
+ *
+ * @param self the object containing the property
+ * @return the property's value
+ * @throws Throwable if any exception occurs while getting the value
+ */
+ public short fget(Object self) throws Throwable {
+ return doGet(self);
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/py/LICENSE b/Ghidra/Features/Pyhidra/src/main/py/LICENSE
new file mode 100644
index 0000000000..c026b6b79a
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/LICENSE
@@ -0,0 +1,11 @@
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/Ghidra/Features/Pyhidra/src/main/py/MANIFEST.in b/Ghidra/Features/Pyhidra/src/main/py/MANIFEST.in
new file mode 100644
index 0000000000..d21308dfc5
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/MANIFEST.in
@@ -0,0 +1,2 @@
+graft tests
+global-exclude *.pyc
diff --git a/Ghidra/Features/Pyhidra/src/main/py/README.md b/Ghidra/Features/Pyhidra/src/main/py/README.md
new file mode 100644
index 0000000000..0905a54f58
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/README.md
@@ -0,0 +1,175 @@
+# pyhidra
+
+Pyhidra is a Python library that provides direct access to the Ghidra API within a native CPython interpreter using [jpype](https://jpype.readthedocs.io/en/latest). As well, Pyhidra contains some conveniences for setting up analysis on a given sample and running a Ghidra script locally. It also contains a Ghidra plugin to allow the use of CPython from the Ghidra user interface.
+
+Pyhidra was initially developed for use with Dragodis and is designed to be installable without requiring Java or Ghidra. This allows other Python projects
+have pyhidra as a dependency and provide optional Ghidra functionality without requiring all users to install Java and Ghidra. It is recommended to recommend that users set the `GHIDRA_INSTALL_DIR` environment variable to simplify locating Ghidra.
+
+
+## Usage
+
+
+### Raw Connection
+
+To get a raw connection to Ghidra use the `start()` function.
+This will setup a Jpype connection and initialize Ghidra in headless mode,
+which will allow you to directly import `ghidra` and `java`.
+
+*NOTE: No projects or programs get setup in this mode.*
+
+```python
+import pyhidra
+pyhidra.start()
+
+import ghidra
+from ghidra.app.util.headless import HeadlessAnalyzer
+from ghidra.program.flatapi import FlatProgramAPI
+from ghidra.base.project import GhidraProject
+from java.lang import String
+
+# do things
+```
+
+### Customizing Java and Ghidra initialization
+
+JVM configuration for the classpath and vmargs may be done through a `PyhidraLauncher`.
+
+```python
+from pyhidra.launcher import HeadlessPyhidraLauncher
+
+launcher = HeadlessPyhidraLauncher()
+launcher.add_classpaths("log4j-core-2.17.1.jar", "log4j-api-2.17.1.jar")
+launcher.add_vmargs("-Dlog4j2.formatMsgNoLookups=true")
+launcher.start()
+```
+
+### Registering an Entry Point
+
+The `PyhidraLauncher` can also be configured through the use of a registered entry point on your own python project.
+This is useful for installing your own Ghidra plugin which uses pyhidra and self-compiles.
+
+First create an [entry_point](https://setuptools.pypa.io/en/latest/userguide/entry_point.html) for `pyhidra.setup`
+pointing to a single argument function which accepts the launcher instance.
+
+```python
+# setup.py
+from setuptools import setup
+
+setup(
+ # ...,
+ entry_points={
+ 'pyhidra.setup': [
+ 'acme_plugin = acme.ghidra_plugin.install:setup',
+ ]
+ }
+)
+```
+
+
+Then we create the target function.
+This function will be called every time a user starts a pyhidra launcher.
+In the same fashion, another entry point `pyhidra.pre_launch` may be registered and will be called after Ghidra and all
+plugins have been loaded.
+
+```python
+# acme/ghidra_plugin/install.py
+from pathlib import Path
+import pyhidra
+
+def setup(launcher):
+ """
+ Run by pyhidra launcher to install our plugin.
+ """
+ launcher.add_classpaths("log4j-core-2.17.1.jar", "log4j-api-2.17.1.jar")
+ launcher.add_vmargs("-Dlog4j2.formatMsgNoLookups=true")
+
+ # Install our plugin.
+ source_path = Path(__file__).parent / "java" / "plugin" # path to uncompiled .java code
+ details = pyhidra.ExtensionDetails(
+ name="acme_plugin",
+ description="My Cool Plugin",
+ author="acme",
+ plugin_version="1.2",
+ )
+ launcher.install_plugin(source_path, details) # install plugin (if not already)
+```
+
+
+### Analyze a File
+
+To have pyhidra setup a binary file for you, use the `open_program()` function.
+This will setup a Ghidra project and import the given binary file as a program for you.
+
+Again, this will also allow you to import `ghidra` and `java` to perform more advanced processing.
+
+```python
+import pyhidra
+
+with pyhidra.open_program("binary_file.exe") as flat_api:
+ program = flat_api.getCurrentProgram()
+ listing = program.getListing()
+ print(listing.getCodeUnitAt(flat_api.toAddr(0x1234)))
+
+ # We are also free to import ghidra while in this context to do more advanced things.
+ from ghidra.app.decompiler.flatapi import FlatDecompilerAPI
+ decomp_api = FlatDecompilerAPI(flat_api)
+ # ...
+ decomp_api.dispose()
+```
+
+By default, pyhidra will run analysis for you. If you would like to do this yourself, set `analyze` to `False`.
+
+```python
+import pyhidra
+
+with pyhidra.open_program("binary_file.exe", analyze=False) as flat_api:
+ from ghidra.program.util import GhidraProgramUtilities
+
+ program = flat_api.getCurrentProgram()
+ if GhidraProgramUtilities.shouldAskToAnalyze(program):
+ flat_api.analyzeAll(program)
+```
+
+
+The `open_program()` function can also accept optional arguments to control the project name and location that gets created.
+(Helpful for opening up a sample in an already existing project.)
+
+```python
+import pyhidra
+
+with pyhidra.open_program("binary_file.exe", project_name="EXAM_231", project_location=r"C:\exams\231") as flat_api:
+ ...
+```
+
+
+### Run a Script
+
+Pyhidra can also be used to run an existing Ghidra Python script directly in your native python interpreter
+using the `run_script()` command.
+However, while you can technically run an existing Ghidra script unmodified, you may
+run into issues due to differences between Jython 2 and CPython 3.
+Therefore, some modification to the script may be needed.
+
+```python
+
+import pyhidra
+
+pyhidra.run_script(r"C:\input.exe", r"C:\some_ghidra_script.py")
+```
+
+This can also be done on the command line using `pyhidra`.
+
+```console
+> pyhidra C:\input.exe C:\some_ghidra_script.py
+```
+
+### Handling Package Name Conflicts
+
+There may be some Python modules and Java packages with the same import path. When this occurs the Python module takes precedence.
+While jpype has its own mechanism for handling this situation, pyhidra automatically makes the Java package accessible by allowing
+it to be imported with an underscore appended to the package name.
+
+```python
+import pdb # imports Python's pdb
+import pdb_ # imports Ghidra's pdb
+```
diff --git a/Ghidra/Features/Pyhidra/src/main/py/pyproject.toml b/Ghidra/Features/Pyhidra/src/main/py/pyproject.toml
new file mode 100644
index 0000000000..9478a8d8b3
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/pyproject.toml
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pyhidra"
+dynamic = ["version", "readme"]
+description = "Native CPython for Ghidra"
+license = {text = "Apache-2.0"}
+requires-python = ">= 3.9"
+authors = [
+ { name = "DC3", email = "dc3.tsd@us.af.mil" },
+]
+maintainers = [
+ { name = "Ghidra Development Team" },
+ { name = "DC3", email = "dc3.tsd@us.af.mil" },
+]
+keywords = [
+ "ghidra",
+]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+]
+dependencies = [
+ "Jpype1>=1.5.0",
+]
+
+[project.optional-dependencies]
+testing = [
+ "pytest",
+ "pytest-datadir",
+]
+
+[project.scripts]
+pyhidra = "pyhidra.__main__:main"
+
+[project.gui-scripts]
+pyhidraw = "pyhidra.gui:_gui"
+
+[project.urls]
+Repository = "https://github.com/NationalSecurityAgency/ghidra"
+
+[tool.setuptools.dynamic]
+version = {attr = "pyhidra.__version__"}
+readme = {file = ["README.md"], content-type = "text/markdown"}
+
+[tool.pytest.ini_options]
+required_plugins = ["pytest-datadir"]
+addopts = "-p no:faulthandler -m \"not plugin\""
+markers = ["plugin"]
diff --git a/Ghidra/Features/Pyhidra/src/main/py/setup.py b/Ghidra/Features/Pyhidra/src/main/py/setup.py
new file mode 100644
index 0000000000..693b5c3f8c
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/setup.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+import sys
+from setuptools import setup
+
+
+if __name__ == "__main__":
+ # This is necessary so that we can build the sdist using pip wheel.
+ # Unfortunately we have to have this work without having setuptools
+ # which pip will install in an isolated environment from the
+ # dependencies directory.
+ if "bdist_wheel" in sys.argv and "sdist" not in sys.argv:
+ sys.argv.append("sdist")
+ setup()
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__init__.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__init__.py
new file mode 100644
index 0000000000..5941eb20f1
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__init__.py
@@ -0,0 +1,53 @@
+
+__version__ = "2.0.0"
+
+# stub for documentation and typing
+# this is mostly to hide the function parameter
+def debug_callback(suspend=False, **kwargs):
+ """
+ Decorator for enabling debugging of functions called from a thread started in Java.
+ All parameters are forwarded to `pydevd.settrace`.
+ It is recommended to remove this decorator from a function when it is no longer needed.
+
+ :param suspend: The suspend parameter for `pydevd.settrace` (Defaults to False)
+ :return: The decorated function
+ """
+
+
+# this is the actual implementation
+def _debug_callback(fun=None, *, suspend=False, **pydevd_kwargs):
+ import functools
+ import sys
+
+ if not fun:
+ return functools.partial(_debug_callback, suspend=suspend, **pydevd_kwargs)
+
+ @functools.wraps(fun)
+ def wrapper(*args, **kwargs):
+ # NOTE: sys.modules is used directly to prevent errors in settrace
+ # the debugger is responsible for connecting so it will have already
+ # been imported
+ pydevd = sys.modules.get("pydevd")
+ if pydevd:
+ pydevd_kwargs["suspend"] = suspend
+ pydevd.settrace(**pydevd_kwargs)
+ return fun(*args, **kwargs)
+
+ return wrapper
+
+
+debug_callback = _debug_callback
+
+
+# Expose API
+from .core import run_script, start, started, open_program
+from .launcher import DeferredPyhidraLauncher, GuiPyhidraLauncher, HeadlessPyhidraLauncher
+from .script import get_current_interpreter
+from .version import ApplicationInfo, ExtensionDetails
+
+
+__all__ = [
+ "debug_callback", "get_current_interpreter", "open_program", "run_script", "start",
+ "started", "ApplicationInfo", "DeferredPyhidraLauncher", "ExtensionDetails",
+ "GuiPyhidraLauncher", "HeadlessPyhidraLauncher"
+]
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__main__.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__main__.py
new file mode 100644
index 0000000000..54cc8c7e3a
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__main__.py
@@ -0,0 +1,273 @@
+import argparse
+import code
+import logging
+
+import sys
+from pathlib import Path
+
+import pyhidra
+import pyhidra.core
+import pyhidra.gui
+
+
+# NOTE: this must be "pyhidra" and not __name__
+logger = logging.getLogger("pyhidra")
+
+
+def _interpreter(interpreter_globals: dict):
+ from ghidra.framework import Application
+ version = Application.getApplicationVersion()
+ name = Application.getApplicationReleaseName()
+ banner = f"Python Interpreter for Ghidra {version} {name}\n"
+ banner += f"Python {sys.version} on {sys.platform}"
+ code.interact(banner=banner, local=interpreter_globals, exitmsg='')
+
+
+# pylint: disable=too-few-public-methods
+class PyhidraArgs(argparse.Namespace):
+ """
+ Custom namespace for holding the command line arguments
+ """
+
+ def __init__(self, parser: argparse.ArgumentParser, **kwargs):
+ super().__init__(**kwargs)
+ self.parser = parser
+ self.valid = True
+ self.verbose = False
+ self.skip_analysis = False
+ self.binary_path: Path = None
+ self.script_path: Path = None
+ self.project_name = None
+ self.project_path: Path = None
+ self.install_dir: Path = None
+ self._script_args = []
+ self.gui = False
+ self.debug = False
+ self._xargs = []
+ self._dargs = []
+
+ def func(self):
+ """
+ Run script or enter repl
+ """
+ if not self.valid:
+ self.parser.print_usage()
+ return
+
+ if self.debug:
+ logger.setLevel(logging.DEBUG)
+
+ vmargs = self.jvm_args
+
+ if self.gui:
+ pyhidra.gui.gui(self.install_dir, vmargs)
+ return
+
+ # not in gui mode so it is easier to start Ghidra now
+ launcher = pyhidra.HeadlessPyhidraLauncher(
+ verbose=self.verbose, install_dir=self.install_dir)
+ launcher.vm_args = vmargs + launcher.vm_args
+ launcher.start()
+
+ if self.script_path is not None:
+ try:
+ pyhidra.run_script(
+ self.binary_path,
+ self.script_path,
+ project_location=self.project_path,
+ project_name=self.project_name,
+ script_args=self._script_args,
+ verbose=self.verbose,
+ analyze=not self.skip_analysis,
+ install_dir=self.install_dir
+ )
+ except KeyboardInterrupt:
+ # gracefully finish when cancelled
+ pass
+ elif self.binary_path is not None:
+ args = (
+ self.binary_path,
+ self.project_path,
+ self.project_name,
+ self.verbose,
+ not self.skip_analysis
+ )
+ with pyhidra.core._flat_api(*args, install_dir=self.install_dir) as api:
+ _interpreter(api)
+ else:
+ _interpreter(globals())
+
+ @property
+ def script_args(self):
+ return self._script_args
+
+ @script_args.setter
+ def script_args(self, value):
+ if self._script_args is None:
+ self._script_args = value
+ else:
+ # append any remaining args to the ones which were previously consumed
+ self._script_args.extend(value)
+
+ @property
+ def jvm_args(self):
+ vmargs = []
+ for arg in self._dargs:
+ vmargs.append("-D" + arg)
+ for arg in self._xargs:
+ vmargs.append("-X" + arg)
+ return vmargs
+
+
+class PathAction(argparse.Action):
+ """
+ Custom action for handling script and binary paths as positional arguments
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.nargs = '*'
+ self.type = str
+
+ def __call__(self, parser, namespace: PyhidraArgs, values, option_string=None):
+
+ if not values:
+ return
+
+ if namespace.script_path is not None:
+ # Any arguments after the script path get passed to the script
+ namespace.script_args = values
+ return
+
+ value = Path(values.pop(0))
+
+ if not value.exists():
+ # File must exist
+ namespace.valid = False
+
+ if value.suffix == ".py":
+ namespace.script_path = value
+ namespace.script_args = values
+ return
+
+ if namespace.binary_path is None:
+ # Peek at the next value, if present, to check if it is a script
+ # The optional binary file MUST come before the script
+ if len(values) > 0 and not values[0].endswith(".py"):
+ namespace.valid = False
+
+ namespace.binary_path = value
+
+ if not values:
+ return
+
+ # Recurse until all values are consumed
+ # The remaining arguments in the ArgParser was a lie for pretty help text
+ # and to pick up trailing optional arguments meant for the script
+ self(parser, namespace, values)
+
+
+def _get_parser():
+ parser = argparse.ArgumentParser(prog="pyhidra")
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ dest="verbose",
+ action="store_true",
+ help="Enable verbose JVM output during Ghidra initialization"
+ )
+ parser.add_argument(
+ "-d",
+ "--debug",
+ default=False,
+ action="store_true",
+ help="Sets the log level to DEBUG"
+ )
+ parser.add_argument(
+ "-g",
+ "--gui",
+ action="store_true",
+ dest="gui",
+ help="Start Ghidra GUI"
+ )
+ parser.add_argument(
+ "--install-dir",
+ type=Path,
+ default=None,
+ dest="install_dir",
+ metavar="",
+ help="Path to Ghidra installation. "
+ "(defaults to the GHIDRA_INSTALL_DIR environment variable)"
+ )
+ parser.add_argument(
+ "--skip-analysis",
+ dest="skip_analysis",
+ action="store_true",
+ help="Switch to skip analysis after loading the binary file if provided"
+ )
+ parser.add_argument(
+ "binary_path",
+ action=PathAction,
+ help="Optional binary path"
+ )
+ parser.add_argument(
+ "script_path",
+ action=PathAction,
+ help=(
+ "Headless script path. The script must have a .py extension. "
+ "If a script is not provided, pyhidra will drop into a repl."
+ )
+ )
+ parser.add_argument(
+ "--project-name",
+ type=str,
+ dest="project_name",
+ metavar="name",
+ help="Project name to use. "
+ "(defaults to binary filename with \"_ghidra\" suffix if provided else None)"
+ )
+ parser.add_argument(
+ "--project-path",
+ type=Path,
+ dest="project_path",
+ metavar="path",
+ help="Location to store project. "
+ "(defaults to same directory as binary file if provided else None)"
+ )
+ parser.add_argument(
+ "-D",
+ dest="_dargs",
+ action="append",
+ metavar="",
+ help="Argument to be forwarded to the JVM"
+ )
+ parser.add_argument(
+ "-X",
+ dest="_xargs",
+ action="append",
+ metavar="",
+ help="Argument to be forwarded to the JVM"
+ )
+ parser.add_argument(
+ "script_args",
+ help="Arguments to be passed to the headless script",
+ nargs=argparse.REMAINDER
+ )
+ return parser
+
+
+def main():
+ """
+ pyhidra module main function
+ """
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter("%(filename)s:%(lineno)d %(message)s")
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ parser = _get_parser()
+ parser.parse_args(namespace=PyhidraArgs(parser)).func()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/converters.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/converters.py
new file mode 100644
index 0000000000..5315e5c17f
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/converters.py
@@ -0,0 +1,14 @@
+
+from pathlib import Path
+
+from jpype import JConversion, JClass
+
+
+@JConversion("java.lang.String", instanceof=Path)
+def pathToString(cls: JClass, path: Path):
+ return cls(path.resolve().__str__())
+
+
+@JConversion("java.io.File", instanceof=Path)
+def pathToFile(cls: JClass, path: Path):
+ return cls(path)
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/core.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/core.py
new file mode 100644
index 0000000000..1d43198235
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/core.py
@@ -0,0 +1,351 @@
+import contextlib
+from pathlib import Path
+from typing import Union, TYPE_CHECKING, Tuple, ContextManager, List, Optional
+
+from pyhidra.converters import * # pylint: disable=wildcard-import, unused-wildcard-import
+
+
+if TYPE_CHECKING:
+ from pyhidra.launcher import PyhidraLauncher
+ from ghidra.base.project import GhidraProject
+ from ghidra.program.flatapi import FlatProgramAPI
+ from ghidra.program.model.lang import CompilerSpec, Language, LanguageService
+ from ghidra.program.model.listing import Program
+
+
+def start(verbose=False, *, install_dir: Path = None) -> "PyhidraLauncher":
+ """
+ Starts the JVM and fully initializes Ghidra in Headless mode.
+
+ :param verbose: Enable verbose output during JVM startup (Defaults to False)
+ :param install_dir: The path to the Ghidra installation directory.
+ (Defaults to the GHIDRA_INSTALL_DIR environment variable)
+ :return: The PhyidraLauncher used to start the JVM
+ """
+ from pyhidra.launcher import HeadlessPyhidraLauncher
+ launcher = HeadlessPyhidraLauncher(verbose=verbose, install_dir=install_dir)
+ launcher.start()
+ return launcher
+
+
+def started() -> bool:
+ """
+ Whether the PyhidraLauncher has already started.
+ """
+ from pyhidra.launcher import PyhidraLauncher
+ return PyhidraLauncher.has_launched()
+
+
+def _get_language(id: str) -> "Language":
+ from ghidra.program.util import DefaultLanguageService
+ from ghidra.program.model.lang import LanguageID, LanguageNotFoundException
+ try:
+ service: "LanguageService" = DefaultLanguageService.getLanguageService()
+ return service.getLanguage(LanguageID(id))
+ except LanguageNotFoundException:
+ # suppress the java exception
+ pass
+ raise ValueError("Invalid Language ID: "+id)
+
+
+def _get_compiler_spec(lang: "Language", id: str = None) -> "CompilerSpec":
+ if id is None:
+ return lang.getDefaultCompilerSpec()
+ from ghidra.program.model.lang import CompilerSpecID, CompilerSpecNotFoundException
+ try:
+ return lang.getCompilerSpecByID(CompilerSpecID(id))
+ except CompilerSpecNotFoundException:
+ # suppress the java exception
+ pass
+ lang_id = lang.getLanguageID()
+ raise ValueError(f"Invalid CompilerSpecID: {id} for Language: {lang_id.toString()}")
+
+
+def _setup_project(
+ binary_path: Union[str, Path],
+ project_location: Union[str, Path] = None,
+ project_name: str = None,
+ language: str = None,
+ compiler: str = None,
+ loader: Union[str, JClass] = None
+) -> Tuple["GhidraProject", "Program"]:
+ from ghidra.base.project import GhidraProject
+ from java.lang import ClassLoader
+ from java.io import IOException
+ if binary_path is not None:
+ binary_path = Path(binary_path)
+ if project_location:
+ project_location = Path(project_location)
+ else:
+ project_location = binary_path.parent
+ if not project_name:
+ project_name = f"{binary_path.name}_ghidra"
+ project_location /= project_name
+ project_location.mkdir(exist_ok=True, parents=True)
+
+ if isinstance(loader, str):
+ from java.lang import ClassNotFoundException
+ try:
+ gcl = ClassLoader.getSystemClassLoader()
+ loader = JClass(loader, gcl)
+ except (TypeError, ClassNotFoundException) as e:
+ raise ValueError from e
+
+ if isinstance(loader, JClass):
+ from ghidra.app.util.opinion import Loader
+ if not Loader.class_.isAssignableFrom(loader):
+ raise TypeError(f"{loader} does not implement ghidra.app.util.opinion.Loader")
+
+ # Open/Create project
+ program: "Program" = None
+ try:
+ project = GhidraProject.openProject(project_location, project_name, True)
+ if binary_path is not None:
+ if project.getRootFolder().getFile(binary_path.name):
+ program = project.openProgram("/", binary_path.name, False)
+ except IOException:
+ project = GhidraProject.createProject(project_location, project_name, False)
+
+ # NOTE: GhidraProject.importProgram behaves differently when a loader is provided
+ # loaderClass may not be null so we must use the correct method override
+
+ if binary_path is not None and program is None:
+ if language is None:
+ if loader is None:
+ program = project.importProgram(binary_path)
+ else:
+ program = project.importProgram(binary_path, loader)
+ if program is None:
+ raise RuntimeError(f"Ghidra failed to import '{binary_path}'. Try providing a language manually.")
+ else:
+ lang = _get_language(language)
+ comp = _get_compiler_spec(lang, compiler)
+ if loader is None:
+ program = project.importProgram(binary_path, lang, comp)
+ else:
+ program = project.importProgram(binary_path, loader, lang, comp)
+ if program is None:
+ message = f"Ghidra failed to import '{binary_path}'. "
+ if compiler:
+ message += f"The provided language/compiler pair ({language} / {compiler}) may be invalid."
+ else:
+ message += f"The provided language ({language}) may be invalid."
+ raise ValueError(message)
+ project.saveAs(program, "/", program.getName(), True)
+
+ return project, program
+
+
+def _setup_script(project: "GhidraProject", program: "Program"):
+ from pyhidra.script import PyGhidraScript
+ from ghidra.app.script import GhidraState
+ from ghidra.program.util import ProgramLocation
+ from ghidra.util.task import TaskMonitor
+
+ from java.io import PrintWriter
+ from java.lang import System
+
+ if project is not None:
+ project = project.getProject()
+
+ location = None
+ if program is not None:
+ # create a GhidraState and setup a HeadlessScript with it
+ mem = program.getMemory().getLoadedAndInitializedAddressSet()
+ if not mem.isEmpty():
+ location = ProgramLocation(program, mem.getMinAddress())
+ state = GhidraState(None, project, program, location, None, None)
+ script = PyGhidraScript()
+ script.set(state, TaskMonitor.DUMMY, PrintWriter(System.out))
+ return script
+
+
+def _analyze_program(flat_api, program):
+ from ghidra.program.util import GhidraProgramUtilities
+ from ghidra.app.script import GhidraScriptUtil
+ if GhidraProgramUtilities.shouldAskToAnalyze(program):
+ GhidraScriptUtil.acquireBundleHostReference()
+ try:
+ flat_api.analyzeAll(program)
+ if hasattr(GhidraProgramUtilities, "markProgramAnalyzed"):
+ GhidraProgramUtilities.markProgramAnalyzed(program)
+ else:
+ GhidraProgramUtilities.setAnalyzedFlag(program, True)
+ finally:
+ GhidraScriptUtil.releaseBundleHostReference()
+
+
+@contextlib.contextmanager
+def open_program(
+ binary_path: Union[str, Path],
+ project_location: Union[str, Path] = None,
+ project_name: str = None,
+ analyze=True,
+ language: str = None,
+ compiler: str = None,
+ loader: Union[str, JClass] = None
+) -> ContextManager["FlatProgramAPI"]: # type: ignore
+ """
+ Opens given binary path in Ghidra and returns FlatProgramAPI object.
+
+ :param binary_path: Path to binary file, may be None.
+ :param project_location: Location of Ghidra project to open/create.
+ (Defaults to same directory as binary file)
+ :param project_name: Name of Ghidra project to open/create.
+ (Defaults to name of binary file suffixed with "_ghidra")
+ :param analyze: Whether to run analysis before returning.
+ :param language: The LanguageID to use for the program.
+ (Defaults to Ghidra's detected LanguageID)
+ :param compiler: The CompilerSpecID to use for the program. Requires a provided language.
+ (Defaults to the Language's default compiler)
+ :param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
+ This may be either a Java class or its path. (Defaults to None)
+ :return: A Ghidra FlatProgramAPI object.
+ :raises ValueError: If the provided language, compiler or loader is invalid.
+ :raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
+ """
+
+ from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher
+
+ if not PyhidraLauncher.has_launched():
+ HeadlessPyhidraLauncher().start()
+
+ from ghidra.app.script import GhidraScriptUtil
+ from ghidra.program.flatapi import FlatProgramAPI
+
+ project, program = _setup_project(
+ binary_path,
+ project_location,
+ project_name,
+ language,
+ compiler,
+ loader
+ )
+ GhidraScriptUtil.acquireBundleHostReference()
+
+ try:
+ flat_api = FlatProgramAPI(program)
+
+ if analyze:
+ _analyze_program(flat_api, program)
+
+ yield flat_api
+ finally:
+ GhidraScriptUtil.releaseBundleHostReference()
+ project.save(program)
+ project.close()
+
+
+@contextlib.contextmanager
+def _flat_api(
+ binary_path: Union[str, Path] = None,
+ project_location: Union[str, Path] = None,
+ project_name: str = None,
+ verbose=False,
+ analyze=True,
+ language: str = None,
+ compiler: str = None,
+ loader: Union[str, JClass] = None,
+ *,
+ install_dir: Path = None
+):
+ """
+ Runs a given script on a given binary path.
+
+ :param binary_path: Path to binary file, may be None.
+ :param script_path: Path to script to run.
+ :param project_location: Location of Ghidra project to open/create.
+ (Defaults to same directory as binary file)
+ :param project_name: Name of Ghidra project to open/create.
+ (Defaults to name of binary file suffixed with "_ghidra")
+ :param script_args: Command line arguments to pass to script.
+ :param verbose: Enable verbose output during Ghidra initialization.
+ :param analyze: Whether to run analysis, if a binary_path is provided, before returning.
+ :param language: The LanguageID to use for the program.
+ (Defaults to Ghidra's detected LanguageID)
+ :param compiler: The CompilerSpecID to use for the program. Requires a provided language.
+ (Defaults to the Language's default compiler)
+ :param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
+ This may be either a Java class or its path. (Defaults to None)
+ :param install_dir: The path to the Ghidra installation directory. This parameter is only
+ used if Ghidra has not been started yet.
+ (Defaults to the GHIDRA_INSTALL_DIR environment variable)
+ :raises ValueError: If the provided language, compiler or loader is invalid.
+ :raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
+ """
+ from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher
+
+ if not PyhidraLauncher.has_launched():
+ HeadlessPyhidraLauncher(verbose=verbose, install_dir=install_dir).start()
+
+ project, program = None, None
+ if binary_path or project_location:
+ project, program = _setup_project(
+ binary_path,
+ project_location,
+ project_name,
+ language,
+ compiler,
+ loader
+ )
+
+ from ghidra.app.script import GhidraScriptUtil
+
+ # always aquire a bundle reference to avoid a NPE when attempting to run any Java scripts
+ GhidraScriptUtil.acquireBundleHostReference()
+ try:
+ script = _setup_script(project, program)
+ if analyze and program is not None:
+ _analyze_program(script, program)
+ yield script
+ finally:
+ GhidraScriptUtil.releaseBundleHostReference()
+ if project is not None:
+ if program is not None:
+ project.save(program)
+ project.close()
+
+
+# pylint: disable=too-many-arguments
+def run_script(
+ binary_path: Optional[Union[str, Path]],
+ script_path: Union[str, Path],
+ project_location: Union[str, Path] = None,
+ project_name: str = None,
+ script_args: List[str] = None,
+ verbose=False,
+ analyze=True,
+ lang: str = None,
+ compiler: str = None,
+ loader: Union[str, JClass] = None,
+ *,
+ install_dir: Path = None
+):
+ """
+ Runs a given script on a given binary path.
+
+ :param binary_path: Path to binary file, may be None.
+ :param script_path: Path to script to run.
+ :param project_location: Location of Ghidra project to open/create.
+ (Defaults to same directory as binary file if None)
+ :param project_name: Name of Ghidra project to open/create.
+ (Defaults to name of binary file suffixed with "_ghidra" if None)
+ :param script_args: Command line arguments to pass to script.
+ :param verbose: Enable verbose output during Ghidra initialization.
+ :param analyze: Whether to run analysis, if a binary_path is provided, before running the script.
+ :param lang: The LanguageID to use for the program.
+ (Defaults to Ghidra's detected LanguageID)
+ :param compiler: The CompilerSpecID to use for the program. Requires a provided language.
+ (Defaults to the Language's default compiler)
+ :param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
+ This may be either a Java class or its path. (Defaults to None)
+ :param install_dir: The path to the Ghidra installation directory. This parameter is only
+ used if Ghidra has not been started yet.
+ (Defaults to the GHIDRA_INSTALL_DIR environment variable)
+ :raises ValueError: If the provided language, compiler or loader is invalid.
+ :raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
+ """
+ script_path = str(script_path)
+ args = binary_path, project_location, project_name, verbose, analyze, lang, compiler, loader
+ with _flat_api(*args, install_dir=install_dir) as script:
+ script.run(script_path, script_args)
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidra_launch.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidra_launch.py
new file mode 100644
index 0000000000..c985f03005
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidra_launch.py
@@ -0,0 +1,103 @@
+import argparse
+import ctypes
+from pathlib import Path
+import sys
+import threading
+
+from .launcher import PyhidraLauncher, _run_mac_app
+
+
+class GhidraLauncher(PyhidraLauncher):
+
+ def __init__(self, verbose=False, class_name=str, gui=False, *, install_dir: Path = None):
+ super().__init__(verbose=verbose, install_dir=install_dir)
+ self._class_name = class_name
+ self._gui = gui
+
+ def _launch(self):
+ from ghidra import Ghidra
+ from java.lang import Runtime, Thread
+
+ if self._gui:
+ if sys.platform == "win32":
+ appid = ctypes.c_wchar_p(self.app_info.name)
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
+ Thread(lambda: Ghidra.main([self._class_name, *self.args])).start()
+ is_exiting = threading.Event()
+ Runtime.getRuntime().addShutdownHook(Thread(is_exiting.set))
+ if sys.platform == "darwin":
+ _run_mac_app()
+ is_exiting.wait()
+ else:
+ Ghidra.main([self._class_name, *self.args])
+
+
+class ParsedArgs(argparse.Namespace):
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.gui = False
+ self._dargs = []
+ self._xargs = []
+ self.install_dir: Path = None
+ self.class_name: str = None
+
+ @property
+ def jvm_args(self):
+ vmargs = []
+ for arg in self._dargs:
+ vmargs.append("-D" + arg)
+ for arg in self._xargs:
+ vmargs.append("-X" + arg)
+ return vmargs
+
+
+def get_parser():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-g",
+ "--gui",
+ action="store_true",
+ dest="gui",
+ help="Start Ghidra GUI"
+ )
+ parser.add_argument(
+ "-D",
+ dest="_dargs",
+ action="append",
+ metavar="",
+ help="Argument to be forwarded to the JVM"
+ )
+ parser.add_argument(
+ "-X",
+ dest="_xargs",
+ action="append",
+ metavar="",
+ help="Argument to be forwarded to the JVM"
+ )
+ parser.add_argument(
+ "--install-dir",
+ type=Path,
+ default=None,
+ dest="install_dir",
+ metavar="",
+ help="Path to Ghidra installation. " \
+ "(defaults to the GHIDRA_INSTALL_DIR environment variable)"
+ )
+ parser.add_argument(
+ "class_name",
+ metavar="class"
+ )
+ return parser
+
+
+if __name__ == "__main__":
+ parser = get_parser()
+
+ args = ParsedArgs()
+ _, remaining = parser.parse_known_args(namespace=args)
+
+ launcher = GhidraLauncher(False, args.class_name, args.gui, install_dir=args.install_dir)
+ launcher.vm_args = args.jvm_args + launcher.vm_args
+ launcher.args = remaining
+ launcher.start()
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidradoc.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidradoc.py
new file mode 100644
index 0000000000..7cef49ca2f
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidradoc.py
@@ -0,0 +1,175 @@
+## ###
+# IP: GHIDRA
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+"""
+Ties the Ghidra documentation into the builtin Python help.
+"""
+
+import json
+from pathlib import Path
+import zipfile
+
+from java.lang import Class
+from java.io import PrintWriter
+from jpype import JMethod, JObject, JClass
+
+from ghidra.framework import Application
+from ghidra.util import SystemUtilities
+
+class _Helper:
+ def __init__(self, stdout: PrintWriter):
+ self.stdout = stdout
+ self.orig_help = help
+ if SystemUtilities.isInHeadlessMode():
+ # ./pythonRun scenario
+ self.msg = "\nExample workflow:\n"
+ self.msg += " # Import headless analyzer\n"
+ self.msg += " from ghidra.app.util.headless import HeadlessAnalyzer\n\n"
+ self.msg += " # View HeadlessAnalyzer API\n"
+ self.msg += " help(HeadlessAnalyzer)\n\n"
+ self.msg += " # Get a HeadlessAnalyzer instance\n"
+ self.msg += " headless = HeadlessAnalyzer.getInstance()\n\n"
+ self.msg += " # Get headless options\n"
+ self.msg += " options = headless.getOptions()\n\n"
+ self.msg += " # View HeadlessOptions API and set options accordingly\n"
+ self.msg += " help(options)\n\n"
+ self.msg += " # View processLocal method API\n"
+ self.msg += " help(headless.processLocal)\n\n"
+ self.msg += " # Perform headless processing\n"
+ self.msg += " headless.processLocal(...)\n\n"
+ else:
+ # PyhidraPlugin scenario
+ self.msg = "Press 'F1' for usage instructions"
+
+ def __call__(self, param=None):
+
+ def get_class_and_method(param):
+ if param is None and not SystemUtilities.isInHeadlessMode():
+ # Enable help() in PyhidraPlugin scenario to show help for GhidraScript
+ return "ghidra.app.script.GhidraScript", None
+ class_name = None
+ method_name = None
+ if isinstance(param, JClass):
+ class_name = param.class_.getName()
+ elif isinstance(param, Class):
+ class_name = param.getName()
+ elif isinstance(param, JMethod):
+ class_name, _, method_name = param.__qualname__.rpartition('.')
+ elif isinstance(param, JObject):
+ class_name = param.getClass().getName()
+ return class_name, method_name
+
+ def get_jsondoc(class_name: str):
+ jsondoc = None
+ try:
+ root = Path(Application.getApplicationRootDirectory().getAbsolutePath()).parent
+ javadoc_zip_name = "GhidraAPI_javadoc.zip"
+ if SystemUtilities.isInDevelopmentMode():
+ javadoc_zip = root / "build" / "tmp" / javadoc_zip_name
+ else:
+ javadoc_zip = root / "docs" / javadoc_zip_name
+ if javadoc_zip.exists():
+ json_path = "api/" + class_name.replace('.', '/') + ".json"
+ with zipfile.ZipFile(javadoc_zip, "r") as docs:
+ with docs.open(json_path) as f:
+ jsondoc = json.load(f)
+ except (IOError, KeyError) as e:
+ pass
+ return jsondoc
+
+ def format_class(cls):
+ sig = "class " + cls['name'] + "\n"
+ if "extends" in cls:
+ sig += " extends " + cls['extends'] + "\n"
+ implements = ", ".join(cls['implements'])
+ if implements:
+ sig += " implements " + implements + " \n"
+ sig += "\n" + cls['comment']
+ return sig
+
+ def format_field(field):
+ sig = f"{field['type_long']} {field['name']}"
+ if field['static']:
+ sig = "static " + sig
+ if constant_value := field['constant_value']:
+ sig += " = " + constant_value
+ sig += "\n"
+ if comment := field['comment']:
+ sig += f" {comment}\n"
+ return sig
+
+ def format_method(method):
+ paramsig = ""
+ args = ""
+ for param in method['params']:
+ if paramsig:
+ paramsig += ", "
+ paramsig += f"{param['type_short']} {param['name']}"
+ args += f" @param {param['name']} ({param['type_long']}): {param['comment']}\n"
+ throws = ""
+ for exception in method['throws']:
+ throws += f" @throws {exception['type_short']}: {exception['comment']}\n"
+ sig = f"{method['return']['type_short']} {method['name']}({paramsig})\n"
+ if method['static']:
+ sig = "static " + sig
+ if comment := method['comment']:
+ desc = f" {comment}\n\n"
+ else:
+ desc = ""
+ ret = ""
+ if method['return']['type_short'] != "void":
+ ret = f" @return {method['return']['type_long']}: {method['return']['comment']}\n"
+ return sig + desc + args + ret + throws
+
+ class_name, method_name = get_class_and_method(param)
+ if class_name is None:
+ self.orig_help(param)
+ else:
+ try_again = True
+ while try_again:
+ try_again = False
+ target = ""
+ if method_name:
+ target = "." + method_name + "()"
+ self.stdout.println("Searching API for " + class_name + target + "...")
+ jsondoc = get_jsondoc(class_name)
+ if jsondoc is None:
+ self.stdout.println("No API found for " + class_name)
+ elif method_name is None:
+ self.stdout.println("#####################################################")
+ self.stdout.println(format_class(jsondoc))
+ self.stdout.println("#####################################################\n")
+ for field in jsondoc['fields']:
+ self.stdout.println(format_field(field))
+ self.stdout.println("-----------------------------------------------------")
+ for method in jsondoc['methods']:
+ self.stdout.println(format_method(method))
+ self.stdout.println("-----------------------------------------------------")
+ else:
+ found_method = False
+ for method in jsondoc['methods']:
+ if method['name'] == method_name:
+ self.stdout.println("-----------------------------------------------------")
+ self.stdout.println(format_method(method))
+ self.stdout.println("-----------------------------------------------------")
+ found_method = True
+ if not found_method:
+ # The method may be inherited, so check for a super class and try again
+ if "extends" in jsondoc:
+ class_name = jsondoc['extends']
+ try_again = True
+
+ def __repr__(self):
+ return self.msg
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/gui.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/gui.py
new file mode 100644
index 0000000000..adbd7cc33c
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/gui.py
@@ -0,0 +1,148 @@
+import argparse
+import io
+import os
+from pathlib import Path
+import platform
+import sys
+import traceback
+from typing import List, NoReturn
+import warnings
+
+import pyhidra
+
+
+class _GuiOutput(io.StringIO):
+
+ def __init__(self, title: str, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.title = title
+
+ def close(self):
+ import tkinter.messagebox
+ tkinter.messagebox.showinfo(self.title, self.getvalue())
+ super().close()
+
+
+class _GuiArgumentParser(argparse.ArgumentParser):
+ def exit(self, status=0, *_):
+ sys.exit(status)
+
+ def print_usage(self, file=None):
+ if file is None:
+ file = _GuiOutput("Usage")
+ self._print_message(self.format_usage(), file)
+
+ def print_help(self, file=None):
+ if file is None:
+ file = _GuiOutput("Help")
+ self._print_message(self.format_help(), file)
+
+
+def _gui_mac() -> NoReturn:
+ args = _parse_args()
+ install_dir = args.install_dir
+ path = Path(sys.base_exec_prefix) / "Resources/Python.app/Contents/MacOS/Python"
+ if path.exists():
+ # the python launcher app will correctly start the venv if sys.executable is in a venv
+ argv = [sys.executable, "-m", "pyhidra", "-g"]
+ if install_dir is not None:
+ argv += ["--install-dir", str(install_dir)]
+ actions = ((os.POSIX_SPAWN_CLOSE, 0), (os.POSIX_SPAWN_CLOSE, 1), (os.POSIX_SPAWN_CLOSE, 2))
+ os.posix_spawn(str(path), argv, os.environ, file_actions=actions)
+ else:
+ print("could not find the Python.app path, launch failed")
+ sys.exit(0)
+
+
+def _parse_args():
+ parser = _GuiArgumentParser(prog="pyhidraw")
+ parser.add_argument(
+ "--install-dir",
+ type=Path,
+ default=None,
+ dest="install_dir",
+ metavar="",
+ help="Path to Ghidra installation. "\
+ "(defaults to the GHIDRA_INSTALL_DIR environment variable)"
+ )
+ return parser.parse_args()
+
+
+def _gui_default(install_dir: Path):
+ pid = os.fork()
+ if pid != 0:
+ # original process can exit
+ return
+
+ fd = os.open(os.devnull, os.O_RDWR)
+ # redirect stdin, stdout and stderr to /dev/null so the jvm can't use the terminal
+ # this also prevents errors from attempting to write to a closed sys.stdout #21
+ os.dup2(fd, sys.stdin.fileno(), inheritable=False)
+ os.dup2(fd, sys.stdout.fileno(), inheritable=False)
+ os.dup2(fd, sys.stderr.fileno(), inheritable=False)
+
+ # run the application
+ gui(install_dir)
+
+
+def _gui():
+ # this is the entry from the gui script
+ # there may or may not be an attached terminal
+ # depending on the current operating system
+
+ if platform.system() == "Darwin":
+ _gui_mac()
+
+ # This check handles the edge case of having a corrupt Python installation
+ # where tkinter can't be imported. Since there may not be an attached
+ # terminal, the problem still needs to be reported somehow.
+ try:
+ import tkinter.messagebox as _
+ except ImportError as e:
+ if platform.system() == "Windows":
+ # there is no console/terminal to report the error
+ import ctypes
+ MessageBox = ctypes.windll.user32.MessageBoxW
+ MessageBox(None, str(e), "Import Error", 0)
+ sys.exit(1)
+ # report this before detaching from the console or no
+ # errors will be reported if they occur
+ raise
+
+ try:
+ args = _parse_args()
+ install_dir = args.install_dir
+ except Exception as e:
+ import tkinter.messagebox
+ msg = "".join(traceback.format_exception(type(e), value=e, tb=e.__traceback__))
+ tkinter.messagebox.showerror(type(e), msg)
+ sys.exit(1)
+
+ if platform.system() == 'Windows':
+ # gui_script works like it is supposed to on windows
+ gui(install_dir)
+ else:
+ _gui_default(install_dir)
+
+
+def gui(install_dir: Path = None, vm_args: List[str] = None):
+ """
+ Starts the Ghidra GUI
+
+ :param install_dir: The path to the Ghidra installation directory.
+ (Defaults to the GHIDRA_INSTALL_DIR environment variable)
+ :param vm_args: Additional vm arguments to be passed ot the JVM.
+ """
+ launcher = pyhidra.GuiPyhidraLauncher(install_dir=install_dir)
+ if vm_args:
+ launcher.vm_args += vm_args
+ launcher.start()
+
+
+def get_current_interpreter():
+ warnings.warn(
+ "get_current_interpreter has been moved. Please use pyhidra.get_current_interpreter",
+ DeprecationWarning
+ )
+ return pyhidra.get_current_interpreter()
+
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/__init__.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/__init__.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/__init__.py
new file mode 100644
index 0000000000..30e66af4a6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/__init__.py
@@ -0,0 +1,3 @@
+"""
+Internal use only
+"""
\ No newline at end of file
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/completions.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/completions.py
new file mode 100644
index 0000000000..6a556bdd4b
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/completions.py
@@ -0,0 +1,104 @@
+import builtins
+from keyword import iskeyword
+from typing import Mapping, Sequence
+from rlcompleter import Completer
+from types import CodeType, FunctionType, MappingProxyType, MethodType, ModuleType
+
+from docking.widgets.label import GLabel
+from generic.theme import GColor
+from ghidra.app.plugin.core.console import CodeCompletion
+from java.util import Arrays, Collections
+from jpype import JPackage
+from jpype.types import JDouble, JFloat, JInt, JLong, JShort
+
+
+NoneType = type(None)
+
+CLASS_COLOR = GColor("color.fg.plugin.python.syntax.class")
+CODE_COLOR = GColor("color.fg.plugin.python.syntax.code")
+FUNCTION_COLOR = GColor("color.fg.plugin.python.syntax.function")
+INSTANCE_COLOR = GColor("color.fg.plugin.python.syntax.instance")
+MAP_COLOR = GColor("color.fg.plugin.python.syntax.map")
+METHOD_COLOR = GColor("color.fg.plugin.python.syntax.method")
+NULL_COLOR = GColor("color.fg.plugin.python.syntax.null")
+NUMBER_COLOR = GColor("color.fg.plugin.python.syntax.number")
+PACKAGE_COLOR = GColor("color.fg.plugin.python.syntax.package")
+SEQUENCE_COLOR = GColor("color.fg.plugin.python.syntax.sequence")
+
+_TYPE_COLORS = {
+ type: CLASS_COLOR,
+ CodeType: CODE_COLOR,
+ FunctionType: FUNCTION_COLOR,
+ dict: MAP_COLOR,
+ MappingProxyType: MAP_COLOR,
+ MethodType: METHOD_COLOR,
+ NoneType: NULL_COLOR,
+ int: NUMBER_COLOR,
+ float: NUMBER_COLOR,
+ complex: NUMBER_COLOR,
+ JShort: NUMBER_COLOR,
+ JInt: NUMBER_COLOR,
+ JLong: NUMBER_COLOR,
+ JFloat: NUMBER_COLOR,
+ JDouble: NUMBER_COLOR,
+ ModuleType: PACKAGE_COLOR,
+ JPackage: PACKAGE_COLOR
+}
+
+
+class PythonCodeCompleter(Completer):
+ """
+ Code Completer for Ghidra's Python interpreter window
+ """
+
+ _BUILTIN_ATTRIBUTE = object()
+ __slots__ = ('cmd',)
+
+ def __init__(self, py_console):
+ super().__init__(py_console.locals.get_static_view())
+ self.cmd: str
+
+ def _get_label(self, i: int) -> GLabel:
+ match = self.matches[i].rstrip("()")
+ label = GLabel(match)
+ attr = self.namespace.get(match, PythonCodeCompleter._BUILTIN_ATTRIBUTE)
+ if attr is PythonCodeCompleter._BUILTIN_ATTRIBUTE:
+ if iskeyword(match.rstrip()):
+ return label
+ attr = builtins.__dict__.get(match, PythonCodeCompleter._BUILTIN_ATTRIBUTE)
+ if attr is not PythonCodeCompleter._BUILTIN_ATTRIBUTE and not match.startswith("__"):
+ attr = builtins.__dict__[match]
+ else:
+ return label
+ color = _TYPE_COLORS.get(type(attr), PythonCodeCompleter._BUILTIN_ATTRIBUTE)
+ if color is PythonCodeCompleter._BUILTIN_ATTRIBUTE:
+ t = type(attr)
+ if isinstance(t, Sequence):
+ color = SEQUENCE_COLOR
+ elif isinstance(t, Mapping):
+ color = MAP_COLOR
+ else:
+ color = INSTANCE_COLOR
+ label.setForeground(color)
+ return label
+
+ def _supplier(self, i: int) -> CodeCompletion:
+ insertion = self.matches[i][len(self.cmd):]
+ return CodeCompletion(self.matches[i], insertion, self._get_label(i))
+
+ def get_completions(self, cmd: str):
+ """
+ Gets all the possible CodeCompletion(s) for the provided cmd
+
+ :param cmd: The code to complete
+ :return: A Java List of all possible CodeCompletion(s)
+ """
+ try:
+ self.cmd = cmd
+ if self.complete(cmd, 0) is None:
+ return Collections.emptyList()
+ res = CodeCompletion[len(self.matches)]
+ Arrays.setAll(res, self._supplier)
+ return Arrays.asList(res)
+ except: # pylint: disable=bare-except
+ return Collections.emptyList()
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/plugin.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/plugin.py
new file mode 100644
index 0000000000..d5f8d9bbc8
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/internal/plugin/plugin.py
@@ -0,0 +1,324 @@
+import contextlib
+import enum
+import inspect
+import logging
+import re
+import sys
+import threading
+import types
+from code import InteractiveConsole
+
+from ghidra.framework import Application
+from ghidra.pyhidra import PyhidraScriptProvider, PyhidraPlugin
+from ghidra.pyhidra.interpreter import PyhidraConsole
+from java.io import BufferedReader, InputStreamReader
+from java.lang import String
+from java.lang import Thread as JThread
+from java.util import Collections
+from java.util.function import Consumer
+from jpype import JClass, JImplements, JOverride
+
+from pyhidra.internal.plugin.completions import PythonCodeCompleter
+from pyhidra.script import PyGhidraScript
+
+
+logger = logging.getLogger(__name__)
+
+
+def _run_script(script):
+ PyGhidraScript(script).run()
+
+
+def _current_thread() -> "PyJavaThread":
+ return threading.current_thread()
+
+
+class ThreadState(enum.Enum):
+ RUNNING = enum.auto()
+ INTERRUPTED = enum.auto()
+ KILLED = enum.auto()
+
+
+def _interpreter_trace(frame: types.FrameType, event: str, _):
+ """
+ Trace function to be used when the interpreter is executing code.
+ This allows it to be interrupted or killed except in native code.
+ """
+ if event == "line":
+ td = _current_thread()
+ if td.killed:
+ sys.exit()
+ if td.interrupted:
+ td.clear_interrupted()
+ raise KeyboardInterrupt()
+ elif event == "call":
+ mod = inspect.getmodule(frame.f_code)
+ if mod:
+ name, _, _ = mod.__name__.partition('.')
+ if name in ("_jpype", "jpype"):
+ # do not trace these functions to avoid raising during
+ # critical python/java bridge functionality
+ return None
+ return _interpreter_trace
+
+
+class PyJavaThread(threading.Thread):
+ """
+ A thread that can be interrupted when running either python or java code
+ """
+
+ def __init__(self, target=None, name=None, args=(), kwargs=None):
+ super().__init__(target=target, name=name, args=args, kwargs=kwargs)
+ self._jthread_lock = threading.Lock()
+ self._jthread = None
+ self._state = ThreadState.RUNNING
+ # preload and initialize these exceptions so that their customizers are applied now
+ # if a python exception is thrown during customization and it will show an unrelated error
+ JClass("java.lang.InterruptedException", initialize=True)
+ JClass("java.nio.channels.ClosedByInterruptException", initialize=True)
+
+ def run(self):
+ try:
+ with self._jthread_lock:
+ JThread.attachAsDaemon()
+ self._jthread = JThread.currentThread()
+ super().run()
+ finally:
+ with self._jthread_lock:
+ if self._jthread and JThread.isAttached():
+ self._jthread = None
+ JThread.detach()
+
+ def interrupt(self):
+ if not self.is_alive():
+ return
+ with self._jthread_lock:
+ if self._jthread:
+ self._jthread.interrupt()
+ self._state = ThreadState.INTERRUPTED
+
+ def clear_interrupted(self):
+ self._state = ThreadState.RUNNING
+
+ def kill(self):
+ if not self.is_alive():
+ return
+ with self._jthread_lock:
+ if self._jthread:
+ self._jthread.interrupt()
+ self._state = ThreadState.KILLED
+
+ @property
+ def interrupted(self) -> bool:
+ return self._state == ThreadState.INTERRUPTED
+
+ @property
+ def killed(self) -> bool:
+ return self._state == ThreadState.KILLED
+
+
+class ConsoleState(enum.Enum):
+ DISPOSING = enum.auto()
+ IDLE = enum.auto()
+ INTERRUPTED = enum.auto()
+ RUNNING = enum.auto()
+ RESET = enum.auto()
+
+
+@JImplements(PyhidraConsole)
+class PyConsole(InteractiveConsole):
+ """
+ Pyhidra Interactive Console
+ """
+
+ _WORD_PATTERN = re.compile(r".*?([\w\.]+)\Z") # get the last word, including '.', from the right
+
+ def __init__(self, py_plugin: PyhidraPlugin):
+ super().__init__(locals=PyGhidraScript(py_plugin.script))
+ appVersion = Application.getApplicationVersion()
+ appName = Application.getApplicationReleaseName()
+ self.banner = f"Python Interpreter for Ghidra {appVersion} {appName}\n" \
+ f"Python {sys.version} on {sys.platform}"
+ console = py_plugin.interpreter.console
+ self._console = py_plugin.interpreter.console
+ self._line_reader = BufferedReader(InputStreamReader(console.getStdin()))
+ self._out = console.getOutWriter()
+ self._err = console.getErrWriter()
+ self._writer = self._out
+ self._thread = None
+ self._interact_thread = None
+ self._script = self.locals._script
+ state = self._script.getState()
+ self._script.set(state, self._out)
+ self._state = ConsoleState.RESET
+ self._completer = PythonCodeCompleter(self)
+
+ def raw_input(self, prompt=''):
+ self._console.setPrompt(prompt)
+ while True:
+ line = self._line_reader.readLine()
+ # NOTE: readLine returns None when interrupted
+ # but also returns "" when an empty line is entered
+ if line is None:
+ if self._state in (ConsoleState.DISPOSING, ConsoleState.RESET):
+ sys.exit()
+ # if we were not reset, read the next line
+ continue
+ if not line:
+ return '\n'
+ return line
+
+ def write(self, data: str):
+ if self._state == ConsoleState.INTERRUPTED:
+ # don't write the traceback from the KeyboardInterrupt
+ return
+ self._writer.write(String @ data)
+ self._writer.flush()
+
+ @JOverride
+ def dispose(self):
+ """
+ Release the console resources
+ """
+ self._state = ConsoleState.DISPOSING
+ self.close()
+ if self._interact_thread:
+ # interact thread may be None if the interpreter was never opened
+ self._interact_thread.join(timeout=1.0)
+ if self._interact_thread.is_alive():
+ logger.debug("PyConsole interact_thread failed to join")
+ self._interact_thread = None
+
+ # release the console reference since it is held by both Python and Java
+ # we are not the owner and are not resposible for disposing it
+ self._console = None
+
+ def close(self):
+ if self._thread:
+ self._thread.kill()
+
+ # closing stdin will wake up any thread attempting to read from it
+ # this is required for the join to complete
+ self._console.getStdin().close()
+
+ # if we timeout then io out of our control is blocking it
+ # at this point we tried and it will complete properly once it stops blocking
+ self._thread.join(timeout=1.0)
+ if self._thread.is_alive():
+ logger.debug("PyConsole execution thread failed to join")
+
+ # ditch the locals so the contents may be released
+ self.locals = dict()
+
+ def reset(self):
+ self._state = ConsoleState.RESET
+ self.close()
+
+ # clear any existing output in the window and re-open the console input
+ self._console.clear()
+
+ # this resets the locals, and gets a new code compiler
+ super().__init__(locals=PyGhidraScript(self._script))
+
+ @property
+ def name(self) -> str:
+ return "Interpreter"
+
+ @JOverride
+ def restart(self):
+ self.reset()
+ if not self._interact_thread:
+ target = self.interact
+ kwargs = {"banner": self.banner}
+ self._interact_thread = threading.Thread(target=target, name=self.name, kwargs=kwargs)
+ self._interact_thread.start()
+
+ @JOverride
+ def interrupt(self):
+ if self._state != ConsoleState.RUNNING:
+ # only interrupt the thread if it is actually running code
+ return
+ if self._thread:
+ self._state = ConsoleState.INTERRUPTED
+ self._thread.interrupt()
+
+ def interact(self, *args, **kwargs):
+ while self._state != ConsoleState.DISPOSING:
+ # We need a nested thread to handle sys.exit which ends the thread.
+ # This is the only way to guarantee the interpreter will never
+ # be left in a dead state.
+ target = super().interact
+ self._thread = PyJavaThread(target=target, name=self.name, args=args, kwargs=kwargs)
+ self._state = ConsoleState.IDLE
+ self._thread.start()
+ self._thread.join()
+ if self._state == ConsoleState.IDLE:
+ # the user used sys.exit and the thread finished
+ # we need to call reset ourselves
+ self.reset()
+
+ @contextlib.contextmanager
+ def redirect_writer(self):
+ self._writer = self._err
+ try:
+ yield
+ finally:
+ self._writer = self._out
+
+ def showsyntaxerror(self, filename=None):
+ with self.redirect_writer():
+ super().showsyntaxerror(filename=filename)
+
+ def showtraceback(self) -> None:
+ with self.redirect_writer():
+ super().showtraceback()
+
+ @contextlib.contextmanager
+ def _run_context(self):
+ self._script.start()
+ success = False
+ try:
+ self._state = ConsoleState.RUNNING
+ sys.settrace(_interpreter_trace)
+ # NOTE: redirect stdout to self so we can flush after each write
+ with contextlib.redirect_stdout(self), contextlib.redirect_stderr(self._err):
+ yield
+ success = True
+ except KeyboardInterrupt:
+ # not always raised even if actually interrupted
+ # catch and use else for consistency
+ raise
+ else:
+ if self._state == ConsoleState.INTERRUPTED:
+ raise KeyboardInterrupt()
+ finally:
+ sys.settrace(None)
+ self._state = ConsoleState.IDLE
+ self._script.end(success)
+ self._out.flush()
+ self._err.flush()
+
+ def runcode(self, code):
+ with self._run_context():
+ super().runcode(code)
+
+ @JOverride
+ def getCompletions(self, cmd: str, pos: int):
+ try:
+ cmd = cmd[:pos]
+ match = self._WORD_PATTERN.match(cmd)
+ if match:
+ cmd = match.group(1)
+ return self._completer.get_completions(cmd)
+ except Exception:
+ return Collections.emptyList()
+
+
+def _init_plugin(plugin: PyhidraPlugin):
+ console = PyConsole(plugin)
+ plugin.interpreter.init(console)
+
+
+def setup_plugin():
+ PyhidraPlugin.setInitializer(Consumer @ _init_plugin)
+ PyhidraScriptProvider.setScriptRunner(Consumer @ _run_script)
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/javac.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/javac.py
new file mode 100644
index 0000000000..98671d68e0
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/javac.py
@@ -0,0 +1,91 @@
+import logging
+import shutil
+import tempfile
+from pathlib import Path
+from os import pathsep
+from typing import List
+
+from jpype import JImplements, JOverride
+
+logger = logging.getLogger(__name__)
+
+
+COMPILER_OPTIONS = ["-target", "21", "-source", "21"]
+
+
+def _to_jar_(jar_path: Path, root: Path):
+ from java.io import ByteArrayOutputStream
+ from java.util.jar import JarEntry, JarOutputStream
+
+ out = ByteArrayOutputStream()
+ with JarOutputStream(out) as jar:
+ for p in root.glob("**/*.class"):
+ p = p.resolve()
+ jar.putNextEntry(JarEntry(str(p.relative_to(root).as_posix())))
+ jar.write(p.read_bytes())
+ jar.closeEntry()
+ jar_path.write_bytes(out.toByteArray())
+
+
+@JImplements("javax.tools.DiagnosticListener", deferred=True)
+class _CompilerDiagnosticListener:
+
+ def __init__(self):
+ from javax.tools import Diagnostic
+ self.errors: List[Diagnostic] = []
+
+ @JOverride
+ def report(self, diagnostic):
+ from javax.tools import Diagnostic
+ diagnostic: Diagnostic = diagnostic
+
+ kind = diagnostic.getKind()
+
+ if kind == Diagnostic.Kind.ERROR:
+ self.errors.append(diagnostic)
+ elif kind == Diagnostic.Kind.WARNING:
+ logger.info(str(kind))
+
+
+def java_compile(src_path: Path, jar_path: Path):
+ """
+ Compiles the provided Java source
+
+ :param src_path: The path to the java file or the root directory of the java source files
+ :param jar_path: The path to write the output jar to
+ :raises ValueError: If an error occurs when compiling the Java source
+ """
+
+ from java.lang import System
+ from java.io import Writer
+ from java.nio.file import Path as JPath
+ from javax.tools import StandardLocation, ToolProvider
+
+ with tempfile.TemporaryDirectory() as out:
+ outdir = Path(out).resolve()
+ compiler = ToolProvider.getSystemJavaCompiler()
+ fman = compiler.getStandardFileManager(None, None, None)
+ cp = [JPath @ (Path(p)) for p in System.getProperty("java.class.path").split(pathsep)]
+ fman.setLocationFromPaths(StandardLocation.CLASS_PATH, cp)
+ if src_path.is_dir():
+ fman.setLocationFromPaths(StandardLocation.SOURCE_PATH, [JPath @ (src_path.resolve())])
+ fman.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, [JPath @ (outdir)])
+ sources = None
+ if src_path.is_file():
+ sources = fman.getJavaFileObjectsFromPaths([JPath @ (src_path)])
+ else:
+ glob = src_path.glob("**/*.java")
+ sources = fman.getJavaFileObjectsFromPaths([JPath @ (p) for p in glob])
+
+ diagnostics = _CompilerDiagnosticListener()
+ task = compiler.getTask(Writer.nullWriter(), fman, diagnostics, COMPILER_OPTIONS, None, sources)
+
+ if not task.call():
+ msg = "\n".join([str(error) for error in diagnostics.errors])
+ raise ValueError(msg)
+
+ if jar_path.suffix == '.jar':
+ jar_path.parent.mkdir(exist_ok=True, parents=True)
+ _to_jar_(jar_path, outdir)
+ else:
+ shutil.copytree(outdir, jar_path, dirs_exist_ok=True)
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/launcher.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/launcher.py
new file mode 100644
index 0000000000..6d43950037
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/launcher.py
@@ -0,0 +1,711 @@
+import contextlib
+import ctypes
+import ctypes.util
+import html
+import importlib.metadata
+import inspect
+import logging
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+from pathlib import Path
+from typing import List, NoReturn, Tuple, Union
+
+import jpype
+from jpype import imports, _jpype
+from importlib.machinery import ModuleSpec
+
+from .javac import java_compile
+from .script import PyGhidraScript
+from .version import ApplicationInfo, ExtensionDetails, MINIMUM_GHIDRA_VERSION
+
+logger = logging.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def _silence_java_output(stdout=True, stderr=True):
+ from java.io import OutputStream, PrintStream
+ from java.lang import System
+ out = System.out
+ err = System.err
+ null = PrintStream(OutputStream.nullOutputStream())
+
+ # The user's Java SecurityManager might not allow this
+ with contextlib.suppress(jpype.JException):
+ if stdout:
+ System.setOut(null)
+ if stderr:
+ System.setErr(null)
+
+ try:
+ yield
+ finally:
+ with contextlib.suppress(jpype.JException):
+ System.setOut(out)
+ System.setErr(err)
+
+
+def _load_entry_points(group: str, *args):
+ """
+ Loads any entry point callbacks registered by external python packages.
+ """
+ try:
+ entries = importlib.metadata.entry_points(group=group)
+ except TypeError:
+ # this is deprecated but the above doesn't work for 3.9
+ entry_points = importlib.metadata.entry_points()
+ if hasattr(entry_points, 'select'):
+ entries = entry_points.select(group=group)
+ else:
+ entries = entry_points.get(group, None)
+ if entries is None:
+ return
+ for entry in entries:
+ name = entry.name
+ try:
+ # Give launcher to callback so they can edit vmargs, install plugins, etc.
+ callback = entry.load()
+ logger.debug(f"Calling {group} entry point: {name}")
+ callback(*args)
+ except Exception as e:
+ logger.error(f"Failed to run {group} entry point {name} with error: {e}")
+
+
+class _PyhidraImportLoader:
+ """ (internal) Finder hook for importlib to handle Python mod conflicts. """
+
+ def find_spec(self, name, path, target=None):
+
+ # If jvm is not started then there is nothing to find.
+ if not _jpype.isStarted():
+ return None
+
+ if name.endswith('_') and _jpype.isPackage(name[:-1]):
+ return ModuleSpec(name, self)
+
+ def create_module(self, spec):
+ return _jpype._JPackage(spec.name[:-1])
+
+ def exec_module(self, fullname):
+ pass
+
+
+@contextlib.contextmanager
+def _plugin_lock():
+ """
+ File lock for processing plugins
+ """
+ from java.io import RandomAccessFile
+ path = Path(tempfile.gettempdir()) / "pyhidra_plugin_lock"
+ try:
+ # Python doesn't have a file lock except for unix systems
+ # so use the one available in Java instead of adding on
+ # a third party library
+ with RandomAccessFile(str(path), "rw") as fp:
+ lock = fp.getChannel().lock()
+ try:
+ yield
+ finally:
+ lock.release()
+ finally:
+ try:
+ path.unlink()
+ except:
+ # if it fails it's ok
+ # another pyhidra process has the lock
+ # it will be removed by said process when done
+ pass
+
+
+class PyhidraLauncher:
+ """
+ Base pyhidra launcher
+ """
+
+ def __init__(self, verbose=False, *, install_dir: Path = None):
+ """
+ Initializes a new `PyhidraLauncher`.
+
+ :param verbose: True to enable verbose output when starting Ghidra.
+ :param install_dir: Ghidra installation directory.
+ (Defaults to the GHIDRA_INSTALL_DIR environment variable)
+ :raises ValueError: If the Ghidra installation directory is invalid.
+ """
+ self._layout = None
+ self._launch_support = None
+ self._java_home = None
+ self._dev_mode = False
+ self._extension_path = None
+
+ install_dir = install_dir or os.getenv("GHIDRA_INSTALL_DIR")
+ self._install_dir = self._validate_install_dir(install_dir)
+
+ # check if we are in the ghidra source tree
+ support = Path(install_dir) / "support"
+ if not support.exists():
+ self._dev_mode = True
+ self._java_home = os.getenv("JAVA_HOME_OVERRIDE")
+
+ self._plugins: List[Tuple[Path, ExtensionDetails]] = []
+ self.verbose = verbose
+
+ ghidra_dir = self._install_dir / "Ghidra"
+ utility_dir = ghidra_dir / "Framework" / "Utility"
+ if self._dev_mode:
+ self._setup_dev_classpath(utility_dir)
+ else:
+ self.class_path = [str(utility_dir / "lib" / "Utility.jar")]
+ self.class_files = []
+ self.vm_args = self._jvm_args()
+ self.args = []
+ self.app_info = ApplicationInfo.from_file(ghidra_dir / "application.properties")
+
+ def _setup_dev_classpath(self, utility_dir: Path):
+ """
+ Sets up the classpath for dev mode as seen in
+ Ghidra/RuntimeScripts/Linux/support/launch.sh
+ """
+ bin_dir = Path("bin") / "main"
+ build_dir = Path("build") / "libs"
+ ls_root = self._install_dir / "GhidraBuild" / "LaunchSupport"
+ classpath = utility_dir / bin_dir
+ launch_support = ls_root / bin_dir
+
+ if not launch_support.exists():
+ classpath = utility_dir / build_dir / "Utility.jar"
+ launch_support = ls_root / build_dir / "LaunchSupport.jar"
+
+ if not launch_support.exists():
+ msg = "Cannot launch from repo because Ghidra has not been compiled " \
+ "with Eclipse or Gradle."
+ self._report_fatal_error("Ghidra not built", msg, ValueError(msg))
+
+ self.class_path = [str(classpath)]
+ if not self._java_home:
+ self._launch_support = launch_support
+
+ def _parse_dev_args(self) -> List[str]:
+ path = self._install_dir / "Ghidra" / "Features" / "Base" / ".launch" / "Ghidra.launch"
+ for line in path.read_text("utf-8").splitlines():
+ if "org.eclipse.jdt.launching.VM_ARGUMENTS" in line:
+ _, _, value = line.partition("value=")
+ value = value.removesuffix("/>")
+ return html.unescape(value).split()
+
+ raise Exception("org.eclipse.jdt.launching.VM_ARGUMENTS not found")
+
+ def _jvm_args(self) -> List[str]:
+ if self._dev_mode and self._java_home:
+ return self._parse_dev_args()
+
+ suffix = "_" + platform.system().upper()
+ if suffix == "_DARWIN":
+ suffix = "_MACOS"
+ option_pattern: re.Pattern = re.compile(fr"VMARGS(?:{suffix})?=(.+)")
+ properties = []
+
+ root = self._install_dir
+
+ if self._dev_mode:
+ root = root / "Ghidra" / "RuntimeScripts" / "Common"
+
+ launch_properties = root / "support" / "launch.properties"
+
+ for line in Path(launch_properties).read_text().splitlines():
+ _, _, override = line.partition("JAVA_HOME_OVERRIDE=")
+ if override:
+ if override.startswith('"') and override.endswith('"'):
+ override = override.removeprefix('"').removesuffix('"')
+ self._java_home = Path(override)
+ continue
+ match = option_pattern.match(line)
+ if match:
+ arg = match.group(1)
+ name, sep, value = arg.partition('=')
+ # unquote any values because quotes are automatically added during JVM startup
+ if value.startswith('"') and value.endswith('"'):
+ value = value.removeprefix('"').removesuffix('"')
+ properties.append(name + sep + value)
+ return properties
+
+ @property
+ def extension_path(self) -> Path:
+ if self._extension_path:
+ return self._extension_path
+ if not self._layout:
+ raise RuntimeError("extension_path cannot be obtained until launcher starts.")
+ # cache the extension path so we can use it after the JVM shuts down during testing
+ self._extension_path = Path(self._layout.getUserSettingsDir().getPath()) / "Extensions"
+ return self._extension_path
+
+ @property
+ def java_home(self) -> Path:
+ if not self._java_home:
+ if self._launch_support:
+ launch_support = self._launch_support
+ else:
+ launch_support = self.install_dir / "support" / "LaunchSupport.jar"
+ if not launch_support.exists():
+ raise ValueError(f"{launch_support} does not exist")
+ cmd = f'java -cp "{launch_support}" LaunchSupport "{self.install_dir}" -jdk_home -save'
+ home = subprocess.check_output(cmd, encoding="utf-8", shell=True)
+ self._java_home = Path(home.rstrip())
+ return self._java_home
+
+ @java_home.setter
+ def java_home(self, path: Path):
+ self._java_home = Path(path)
+
+ @property
+ def install_dir(self) -> Path:
+ return self._install_dir
+
+ @classmethod
+ def _validate_install_dir(cls, install_dir: Union[Path, str]) -> Path:
+ """
+ Validates and sets the Ghidra installation directory.
+ """
+ if not install_dir:
+ msg = (
+ "Please set the GHIDRA_INSTALL_DIR environment variable "
+ "or `install_dir` during the Launcher construction to the "
+ "directory where Ghidra is installed."
+ )
+ cls._report_fatal_error("GHIDRA_INSTALL_DIR is not set", msg, ValueError(msg))
+
+ # both the directory and the application.properties file must exist
+ install_dir = Path(install_dir)
+ if not install_dir.exists():
+ msg = f"{install_dir} does not exist"
+ cls._report_fatal_error("Invalid Ghidra Installation Directory", msg, ValueError(msg))
+
+ path = install_dir / "Ghidra" / "application.properties"
+ if not path.exists():
+ msg = "The Ghidra installation does not contain the required " + \
+ "application.properties file"
+ cls._report_fatal_error("Corrupt Ghidra Installation", msg, ValueError(msg))
+
+ support = install_dir / "support"
+
+ if not support.exists():
+ # dev mode
+ return install_dir
+
+ path = install_dir / "Ghidra" / "Features" / "Pyhidra" / "lib" / "Pyhidra.jar"
+
+ if not path.exists():
+ msg = "The Ghidra installation does not contain the Pyhidra module\n" + \
+ f"{path} does not exist"
+ cls._report_fatal_error("Incorrect Ghidra installation directory", msg, ValueError(msg))
+
+ return install_dir
+
+ def add_classpaths(self, *args):
+ """
+ Add additional entries to the classpath when starting the JVM
+ """
+ self.class_path += args
+
+ def add_vmargs(self, *args):
+ """
+ Add additional vmargs for launching the JVM
+ """
+ self.vm_args += args
+
+ def add_class_files(self, *args):
+ """
+ Add additional entries to be added the classpath after Ghidra has been fully loaded.
+ This ensures that all of Ghidra is available so classes depending on it can be properly loaded.
+ """
+ self.class_files += args
+
+ @classmethod
+ def _report_fatal_error(cls, title: str, msg: str, cause: Exception) -> NoReturn:
+ logger.error("%s: %s", title, msg)
+ raise cause
+
+ def check_ghidra_version(self):
+ """
+ Checks if the currently installed Ghidra version is supported.
+ The launcher will report the problem and terminate if it is not supported.
+ """
+ if self.app_info.version < MINIMUM_GHIDRA_VERSION:
+ msg = f"Ghidra version {self.app_info.version} is not supported" + os.linesep + \
+ f"The minimum required version is {MINIMUM_GHIDRA_VERSION}"
+ self._report_fatal_error("Unsupported Version", msg, ValueError(msg))
+
+ def _setup_java(self, **jpype_kwargs):
+ """
+ Run setup entry points, start the JVM and prepare ghidra imports
+ """
+ # Before starting up, give launcher to installed entry points so they can do their thing.
+ _load_entry_points("pyhidra.setup", self)
+
+ # Merge classpath
+ jpype_kwargs['classpath'] = self.class_path + jpype_kwargs.get('classpath', [])
+
+ # force convert strings (required by pyhidra)
+ jpype_kwargs['convertStrings'] = True
+
+ # set the JAVA_HOME environment variable to the correct one so jpype uses it
+ os.environ['JAVA_HOME'] = str(self.java_home)
+
+ jpype_kwargs['ignoreUnrecognized'] = True
+
+ if os.getenv("PYHIDRA_DEBUG"):
+ debug = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:18001"
+ self.vm_args.insert(0, debug)
+
+ jpype.startJVM(
+ None, # indicates to use JAVA_HOME as the jvm path
+ *self.vm_args,
+ **jpype_kwargs
+ )
+
+ # Install hook into python importlib
+ sys.meta_path.append(_PyhidraImportLoader())
+
+ imports.registerDomain("ghidra")
+
+ def _pre_launch_init(self):
+ """
+ Prepare registered plugins and initialize the Ghidra environment
+ """
+
+ # import and create a temporary GhidraApplicationLayout this can be
+ # used without initializing Ghidra to obtain the correct Extension path
+ from ghidra import GhidraApplicationLayout
+ self._layout = GhidraApplicationLayout()
+
+
+
+ # remove any installed pyhidra extension
+ # if left in place Ghidra will fail to start with a confusing
+ # and unrelated error about the InterpreterConsole class not being found
+ # this is only needed for those using a DEV build of Ghidra
+ # who also have pyhidra installed.
+ # however, this took an unnecessary amount of time to debug
+ self.uninstall_plugin("pyhidra")
+
+ # uninstall any outdated plugins before initializing
+ # Ghidra to ensure they are loaded correctly
+ for _, details in self._plugins:
+ try:
+ self._uninstall_old_plugin(details)
+ except:
+ logger.warning("failed to uninstall plugin %s", details.name)
+
+ from ghidra import GhidraLauncher
+ self._layout = GhidraLauncher.initializeGhidraEnvironment()
+
+ # import it at the end so interfaces in our java code may be implemented
+ from pyhidra.internal.plugin.plugin import setup_plugin
+ setup_plugin()
+
+ # Add extra class paths
+ # Do this before installing plugins incase dependencies are needed
+ if self.class_files:
+ from java.lang import ClassLoader
+ gcl = ClassLoader.getSystemClassLoader()
+ for path in self.class_files:
+ gcl.addPath(path)
+
+ needs_reload = False
+
+ # Install extra plugins.
+ for source_path, details in self._plugins:
+ try:
+ needs_reload = self._install_plugin(source_path, details) or needs_reload
+ except Exception as e:
+ # we should always warn if a plugin failed to compile
+ logger.warning(e, exc_info=e)
+
+ if needs_reload:
+ # "restart" Ghidra
+ self._layout = GhidraLauncher.initializeGhidraEnvironment()
+
+ # import properties to register the property customizer
+ from . import properties as _
+
+ _load_entry_points("pyhidra.pre_launch")
+
+ def start(self, **jpype_kwargs):
+ """
+ Starts Jpype connection to Ghidra (if not already started).
+ """
+ if jpype.isJVMStarted():
+ return
+
+ self.check_ghidra_version()
+
+ try:
+ self._setup_java(**jpype_kwargs)
+ with _plugin_lock():
+ self._pre_launch_init()
+ self._launch()
+ except Exception as e:
+ self._report_fatal_error("An error occured launching Ghidra", str(e), e)
+
+ def get_install_path(self, plugin_name: str) -> Path:
+ """
+ Obtains the path for installation of a given plugin.
+ """
+ return self.extension_path / plugin_name
+
+ def _get_plugin_jar_path(self, plugin_name: str) -> Path:
+ return self.get_install_path(plugin_name) / "lib" / (plugin_name + ".jar")
+
+ def uninstall_plugin(self, plugin_name: str):
+ """
+ Uninstalls given plugin.
+ """
+ path = self.get_install_path(plugin_name)
+ if path.exists():
+ # delete the existing extension so it will be up-to-date
+ shutil.rmtree(path)
+
+ def _uninstall_old_plugin(self, details: ExtensionDetails):
+ """
+ Automatically uninstalls an outdated plugin if it exists.
+ """
+ plugin_name = details.name
+ path = self.get_install_path(plugin_name)
+ ext = path / "extension.properties"
+
+ # Uninstall old version.
+ if path.exists() and ext.exists():
+ orig_details = ExtensionDetails.from_file(ext)
+ if not orig_details.plugin_version or orig_details.plugin_version != details.plugin_version:
+ try:
+ self.uninstall_plugin(plugin_name)
+ except Exception as e:
+ logger.warning("Could not delete existing plugin at %s", path, exc_info=e)
+ else:
+ logger.info(f"Uninstalled older plugin: {plugin_name} {orig_details.plugin_version}")
+
+ def _install_plugin(self, source_path: Path, details: ExtensionDetails):
+ """
+ Compiles and installs a Ghidra extension if not already installed.
+ """
+ # No clunky plugin building required
+ # `pip install *` and done
+ if details.version is None:
+ details.version = self.app_info.version
+ plugin_name = details.name
+ path = self.get_install_path(plugin_name)
+ ext = path / "extension.properties"
+ manifest = path / "Module.manifest"
+ root = source_path
+ jar_path = path / "lib" / (plugin_name + ".jar")
+
+ if not jar_path.exists():
+ path.mkdir(parents=True, exist_ok=True)
+
+ try:
+ java_compile(root, jar_path)
+ except:
+ shutil.rmtree(path, ignore_errors=True)
+ raise
+
+ ext.write_text(str(details))
+
+ # required empty file
+ manifest.touch()
+
+ # Copy over ghidra_scripts if included.
+ ghidra_scripts = root / "ghidra_scripts"
+ if ghidra_scripts.exists():
+ shutil.copytree(ghidra_scripts, path / "ghidra_scripts")
+
+ logger.info(f"Installed plugin: {plugin_name} {details.plugin_version}")
+ return True
+
+ return False
+
+ def install_plugin(self, source_path: Path, details: ExtensionDetails):
+ """
+ Compiles and installs a Ghidra extension when launcher is started.
+ """
+ self._plugins.append((source_path, details))
+
+ def _launch(self):
+ pass
+
+ @staticmethod
+ def has_launched() -> bool:
+ """
+ Checks if jpype has started and if Ghidra has been fully initialized.
+ """
+ if not jpype.isJVMStarted():
+ return False
+
+ from ghidra.framework import Application
+ return Application.isInitialized()
+
+
+class DeferredPyhidraLauncher(PyhidraLauncher):
+ """
+ PyhidraLauncher which allows full Ghidra initialization to be deferred.
+ initialize_ghidra must be called before all Ghidra classes are fully available.
+ """
+
+ def initialize_ghidra(self, headless=True):
+ """
+ Finished Ghidra initialization
+
+ :param headless: whether or not to initialize Ghidra in headless mode.
+ (Defaults to True)
+ """
+ from ghidra import GhidraRun
+ from ghidra.framework import Application, HeadlessGhidraApplicationConfiguration
+ with _silence_java_output(not self.verbose, not self.verbose):
+ if headless:
+ config = HeadlessGhidraApplicationConfiguration()
+ Application.initializeApplication(self._layout, config)
+ else:
+ GhidraRun().launch(self._layout, self.args)
+
+
+class HeadlessPyhidraLauncher(PyhidraLauncher):
+ """
+ Headless pyhidra launcher
+ """
+
+ def _launch(self):
+ from ghidra.framework import Application, HeadlessGhidraApplicationConfiguration
+ with _silence_java_output(not self.verbose, not self.verbose):
+ config = HeadlessGhidraApplicationConfiguration()
+ Application.initializeApplication(self._layout, config)
+
+
+class _PyhidraStdOut:
+
+ def __init__(self, stream):
+ self._stream = stream
+
+ def _get_current_script(self) -> "PyGhidraScript":
+ for entry in inspect.stack():
+ f_globals = entry.frame.f_globals
+ if isinstance(f_globals, PyGhidraScript):
+ return f_globals
+
+ def flush(self):
+ script = self._get_current_script()
+ if script is not None:
+ writer = script._script.writer
+ if writer is not None:
+ writer.flush()
+ return
+
+ self._stream.flush()
+
+ def write(self, s: str) -> int:
+ script = self._get_current_script()
+ if script is not None:
+ writer = script._script.writer
+ if writer is not None:
+ writer.write(s)
+ return len(s)
+
+ return self._stream.write(s)
+
+
+class GuiPyhidraLauncher(PyhidraLauncher):
+ """
+ GUI pyhidra launcher
+ """
+
+ @classmethod
+ def popup_error(cls, header: str, msg: str) -> NoReturn:
+ import tkinter.messagebox
+ tkinter.messagebox.showerror(header, msg)
+ sys.exit()
+
+ @classmethod
+ def _report_fatal_error(cls, title: str, msg: str, cause: Exception) -> NoReturn:
+ logger.exception(cause, exc_info=cause)
+ cls.popup_error(title, msg)
+
+ @staticmethod
+ def _get_thread(name: str):
+ from java.lang import Thread
+ for t in Thread.getAllStackTraces().keySet():
+ if t.getName() == name:
+ return t
+ return None
+
+ def _launch(self):
+ from ghidra import Ghidra
+ from java.lang import Runtime, Thread
+
+ if sys.platform == "win32":
+ appid = ctypes.c_wchar_p(self.app_info.name)
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
+
+ stdout = _PyhidraStdOut(sys.stdout)
+ stderr = _PyhidraStdOut(sys.stderr)
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
+ Thread(lambda: Ghidra.main(["ghidra.GhidraRun", *self.args])).start()
+ is_exiting = threading.Event()
+ Runtime.getRuntime().addShutdownHook(Thread(is_exiting.set))
+ if sys.platform == "darwin":
+ _run_mac_app()
+ is_exiting.wait()
+
+
+def _run_mac_app():
+ # this runs the event loop
+ # it is required for the GUI to show up
+ from ctypes import c_void_p, c_double, c_uint64, c_int64, c_int32, c_bool, CFUNCTYPE
+
+ CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
+
+ def get_function(name, restype, *argtypes):
+ res = getattr(CoreFoundation, name)
+ res.argtypes = [arg for arg in argtypes]
+ res.restype = restype
+ return res
+
+ CFRunLoopTimerCallback = CFUNCTYPE(None, c_void_p, c_void_p)
+ kCFRunLoopDefaultMode = c_void_p.in_dll(CoreFoundation, "kCFRunLoopDefaultMode")
+ kCFRunLoopRunFinished = c_int32(1)
+ NULL = c_void_p(0)
+ INF_TIME = c_double(1.0e20)
+ FIRE_ONCE = c_double(0)
+ kCFAllocatorDefault = NULL
+
+ CFRunLoopGetCurrent = get_function("CFRunLoopGetCurrent", c_void_p)
+ CFRelease = get_function("CFRelease", None, c_void_p)
+
+ CFRunLoopTimerCreate = get_function(
+ "CFRunLoopTimerCreate",
+ c_void_p,
+ c_void_p,
+ c_double,
+ c_double,
+ c_uint64,
+ c_int64,
+ CFRunLoopTimerCallback,
+ c_void_p
+ )
+
+ CFRunLoopAddTimer = get_function("CFRunLoopAddTimer", None, c_void_p, c_void_p, c_void_p)
+ CFRunLoopRunInMode = get_function("CFRunLoopRunInMode", c_int32, c_void_p, c_double, c_bool)
+
+ @CFRunLoopTimerCallback
+ def dummy_timer(timer, info):
+ # this doesn't need to do anything
+ # CFRunLoopTimerCreate just needs a valid callback
+ return
+
+ timer = CFRunLoopTimerCreate(kCFAllocatorDefault, INF_TIME, FIRE_ONCE, 0, 0, dummy_timer, NULL)
+ CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode)
+ CFRelease(timer)
+
+ while CFRunLoopRunInMode(kCFRunLoopDefaultMode, INF_TIME, False) != kCFRunLoopRunFinished:
+ pass
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/properties.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/properties.py
new file mode 100644
index 0000000000..61008f9035
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/properties.py
@@ -0,0 +1,49 @@
+import inspect
+import keyword
+import logging
+
+import jpype
+
+
+# pylint: disable=no-member, too-few-public-methods
+@jpype.JImplementationFor("java.lang.Object")
+class _JavaObject:
+
+ def __jclass_init__(self: jpype.JClass):
+ try:
+ if isinstance(self, jpype.JException):
+ # don't process any exceptions
+ return
+ exposer = jpype.JClass("ghidra.pyhidra.PythonFieldExposer")
+ if exposer.class_.isAssignableFrom(self.class_):
+ return
+ utils = jpype.JClass("ghidra.pyhidra.property.PropertyUtils")
+ for prop in utils.getProperties(self.class_):
+ field = prop.field
+ if keyword.iskeyword(field):
+ field += '_'
+ if field == "class_":
+ continue
+ # check for existing inherited properties
+ existing = inspect.getattr_static(self, field, None)
+ fget = None
+ fset = None
+ if prop.hasGetter():
+ fget = prop.fget
+ elif existing and hasattr(existing, "fget"):
+ fget = existing.fget
+ if prop.hasSetter():
+ fset = prop.fset
+ elif existing and hasattr(existing, "fset"):
+ fset = existing.fset
+ self._customize(field, property(fget, fset))
+
+ # allowing any exception to escape here causes the traceback to be lost
+ # log it here so we can figure out what happened
+ # pylint: disable=bare-except
+ except:
+ logger = logging.getLogger(__name__)
+ logger.error("Failed to add property customizations for %s", self, exc_info=1)
+
+ def __repr__(self):
+ return str(self)
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/script.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/script.py
new file mode 100644
index 0000000000..9e4a79e1d6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/script.py
@@ -0,0 +1,300 @@
+import functools
+import importlib
+import importlib.util
+import inspect
+import logging
+import sys
+import traceback
+from collections.abc import ItemsView, KeysView
+from importlib.machinery import ModuleSpec, SourceFileLoader
+from pathlib import Path
+from jpype import JClass, JImplementationFor
+from typing import List
+
+
+from pyhidra import debug_callback
+
+_NO_ATTRIBUTE = object()
+
+_headless_interpreter = None
+
+
+class _StaticMap(dict):
+ # this is a special view of the PyGhidraScript for use with rlcompleter
+
+ __slots__ = ('script',)
+
+ def __init__(self, script: "PyGhidraScript"):
+ super().__init__()
+ self.script = script
+
+ def __getitem__(self, key):
+ res = self.get(key, _NO_ATTRIBUTE)
+ if res is not _NO_ATTRIBUTE:
+ if isinstance(res, property):
+ # rlcompleter is attempting to use a property getter on the interpreter script
+ # allow the property magic to take place
+ # this is necessary for completions on currentAddress, currentProgram, etc.
+ try:
+ return getattr(self.script, key)
+ except AttributeError:
+ return res
+ return res
+ raise KeyError(key)
+
+ def get(self, key, default=None):
+ res = self.script.get_static(key)
+ return res if res is not _NO_ATTRIBUTE else default
+
+ def __iter__(self):
+ yield from self.script
+
+ def keys(self):
+ return KeysView(self)
+
+ def items(self):
+ return ItemsView(self)
+
+
+class _JavaProperty(property):
+
+ def __init__(self, field):
+ super().__init__()
+ self._field = field
+
+ def __get__(self, obj, cls):
+ return self._field.fget(obj)
+
+ def __set__(self, obj, value):
+ self._field.fset(obj, value)
+
+
+#pylint: disable=too-few-public-methods
+@JImplementationFor("ghidra.pyhidra.PythonFieldExposer")
+class _PythonFieldExposer:
+
+ #pylint: disable=no-member
+ def __jclass_init__(self):
+ exposer = JClass("ghidra.pyhidra.PythonFieldExposer")
+ if self.class_ == exposer:
+ return
+ try:
+ for k, v in exposer.getProperties(self.class_).items():
+ self._customize(k, _JavaProperty(v))
+ # pylint: disable=bare-except
+ except:
+ logger = logging.getLogger(__name__)
+ logger.error("Failed to add property customizations for %s", self, exc_info=1)
+
+
+class _GhidraScriptModule:
+
+ def __init__(self, spec: ModuleSpec):
+ super().__setattr__("__dict__", spec.loader_state["script"])
+
+ def __setattr__(self, attr, value):
+ if hasattr(self, attr):
+ raise AttributeError(f"readonly attribute {attr}")
+ super().__setattr__(attr, value)
+
+
+class _GhidraScriptLoader(SourceFileLoader):
+
+ def __init__(self, script: "PyGhidraScript", spec: ModuleSpec):
+ super().__init__(spec.name, spec.origin)
+ spec.loader_state = {"script": script}
+
+ def create_module(self, spec: ModuleSpec):
+ return _GhidraScriptModule(spec)
+
+ # this will make debugging "just work" if a debugger attaches to the process
+ @debug_callback
+ def exec_module(self, module):
+ return super().exec_module(module)
+
+
+def _build_script_print(stdout):
+ @functools.wraps(print)
+ def wrapper(*objects, sep=' ', end='\n', file=None, flush=False):
+ # ensure we get the same behavior if the file is closed
+ if file is None:
+ file = stdout
+ # since write will be used, it won't flush on a line ending
+ # force it for stdout in a GhidraScript
+ flush = flush or end == '\n'
+ return print(*objects, sep=sep, end=end, file=file, flush=flush)
+ return wrapper
+
+
+# pylint: disable=missing-function-docstring
+class PyGhidraScript(dict):
+ """
+ Python GhidraScript Wrapper
+ """
+
+ def __init__(self, jobj=None):
+ super().__init__()
+ if jobj is None:
+ from ghidra.pyhidra import PyhidraScriptProvider
+ jobj = PyhidraScriptProvider().getScriptInstance(None, None)
+ self._script = jobj
+
+ global _headless_interpreter
+
+ from ghidra.util import SystemUtilities
+ from .ghidradoc import _Helper
+
+ if SystemUtilities.isInHeadlessMode() and _headless_interpreter is None:
+ _headless_interpreter = jobj
+
+ # ensure the builtin set takes precedence over GhidraScript.set
+ super().__setitem__("set", set)
+
+ super().__setitem__("__this__", self._script)
+
+ # this is injected since Ghidra commit e66e72577ded1aeae53bcc3f361dfce1ecf6e24a
+ super().__setitem__("this", self._script)
+
+ # overwrite the builtin print so it will always work
+ # the global redirection of stdout/stderr works on a best-effort basis
+ printer = _build_script_print(self._script.writer)
+ super().__setitem__("print", printer)
+
+ super().__setitem__("help", _Helper(self._script.writer))
+
+ def __missing__(self, k):
+ attr = getattr(self._script, k, _NO_ATTRIBUTE)
+ if attr is not _NO_ATTRIBUTE:
+ return attr
+ raise KeyError(k)
+
+ def __getattr__(self, item):
+ return getattr(self._script, item)
+
+ def __setitem__(self, k, v):
+ attr = inspect.getattr_static(self._script, k, _NO_ATTRIBUTE)
+ if attr is not _NO_ATTRIBUTE and isinstance(attr, property):
+ setattr(self._script, k, v)
+ else:
+ super().__setitem__(k, v)
+
+ def __iter__(self):
+ yield from super().__iter__()
+ yield from dir(self._script)
+
+ def get_static(self, key):
+ res = self.get(key, _NO_ATTRIBUTE)
+ if res is not _NO_ATTRIBUTE:
+ return res
+ return inspect.getattr_static(self._script, key, _NO_ATTRIBUTE)
+
+ def get_static_view(self):
+ return _StaticMap(self)
+
+ def set(self, state, monitor, writer):
+ """
+ see GhidraScript.set
+ """
+ self._script.set(state, monitor, writer)
+
+ def run(self, script_path: str = None, script_args: List[str] = None):
+ """
+ Run this GhidraScript
+
+ :param script_path: The path of the python script
+ :param script_args: The arguments for the python script
+ """
+ sf = self._script.getSourceFile()
+ if sf is None and script_path is None:
+ return
+ if script_path is None:
+ script_path = sf.getAbsolutePath()
+ script_args = self._script.getScriptArgs()
+
+ if script_args is None:
+ script_args = []
+ else:
+ self._script.setScriptArgs(script_args)
+
+ orig_argv = sys.argv
+ script_root = str(Path(script_path).parent)
+
+ # honor the python safe_path flag introduced in 3.11
+ safe_path = bool(getattr(sys.flags, "safe_path", 0))
+ try:
+ # Temporarily set command line arguments.
+ sys.argv = [script_path] + list(script_args)
+
+ if not safe_path:
+ # add the directory containing the script to the start of the path
+ # this provides the same import behavior as if the script was run normally
+ sys.path.insert(0, script_root)
+
+ spec = importlib.util.spec_from_file_location('__main__', script_path)
+ spec.loader = _GhidraScriptLoader(self, spec)
+ m = importlib.util.module_from_spec(spec)
+ try:
+ spec.loader.exec_module(m)
+ # pylint: disable=bare-except
+ except:
+ # filter the traceback so that it stops at the script
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ i = 0
+ tb = traceback.extract_tb(exc_tb)
+ for fs in tb:
+ if fs.filename == script_path:
+ break
+ i += 1
+ ss = traceback.StackSummary.from_list(tb[i:])
+ e = traceback.TracebackException(exc_type, exc_value, exc_tb)
+ e.stack = ss
+ self._script.printerr(''.join(e.format()))
+ finally:
+ sys.argv = orig_argv
+
+ if not safe_path:
+ sys.path.remove(script_root)
+
+
+def get_current_interpreter():
+ """
+ Gets the underlying GhidraScript for the focused Pyhidra InteractiveConsole.
+ This will always return None unless it is being access from a function
+ called from within the interactive console.
+
+ :return: The GhidraScript for the active interactive console.
+ """
+
+ try:
+ from ghidra.util import SystemUtilities
+ from ghidra.framework.main import AppInfo
+
+ global _headless_interpreter
+
+ if SystemUtilities.isInHeadlessMode():
+ if _headless_interpreter is None:
+ # one hasn't been created yet so make one now
+ PyhidraScriptProvider = JClass("ghidra.pyhidra.PyhidraScriptProvider")
+ _headless_interpreter = PyhidraScriptProvider.PyhidraHeadlessScript()
+ return _headless_interpreter
+
+ project = AppInfo.getActiveProject()
+ if project is None:
+ return None
+
+ ts = project.getToolServices()
+ tool = None
+ for t in ts.getRunningTools():
+ if t.getActiveWindow().isFocused():
+ tool = t
+ break
+
+ if tool is None:
+ return None
+
+ for plugin in tool.getManagedPlugins():
+ if plugin.name == 'PyhidraPlugin':
+ return plugin.script
+
+ except ImportError:
+ return None
diff --git a/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/version.py b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/version.py
new file mode 100644
index 0000000000..c43f47036b
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/version.py
@@ -0,0 +1,74 @@
+
+import dataclasses
+from datetime import datetime
+from pathlib import Path
+import re
+
+
+MINIMUM_GHIDRA_VERSION = "11.2"
+_APPLICATION_PATTERN = re.compile(r"^application\.(\S+?)=(.*)$")
+
+
+@dataclasses.dataclass(frozen=True)
+class ApplicationInfo:
+ """
+ Ghidra Application Properties
+ """
+ name: str
+ version: str
+ release_name: str
+ revision_ghidra: str = ""
+ build_date: str = ""
+ build_date_short: str = ""
+ layout_version: str = ""
+ gradle_min: str = ""
+ java_min: str = ""
+ java_max: str = ""
+ java_compiler: str = ""
+ gradle_max: str = ""
+
+ @classmethod
+ def from_file(cls, file: Path):
+ """
+ Parses Ghidra's application.properties file from the provided path
+ """
+ valid_fields = {f.name for f in dataclasses.fields(cls)}
+ kwargs = dict()
+ for line in file.read_text(encoding="utf8").splitlines():
+ match = _APPLICATION_PATTERN.match(line)
+ if not match:
+ continue
+ attr = match.group(1).replace('.', '_').replace('-', '_')
+ value = match.group(2)
+ if attr in valid_fields:
+ kwargs[attr] = value
+ return cls(**kwargs)
+
+
+@dataclasses.dataclass
+class ExtensionDetails:
+ """
+ Python side ExtensionDetails
+ """
+ name: str
+ description: str
+ author: str
+ createdOn: str = dataclasses.field(default_factory=lambda: str(datetime.now()))
+ version: str = None
+ plugin_version: str = "0.0.1"
+
+ @classmethod
+ def from_file(cls, ext_path: Path):
+ valid_fields = {f.name for f in dataclasses.fields(cls)}
+ def cast(key, value):
+ return cls.__annotations__[key](value)
+ lines = ext_path.read_text().splitlines()
+ kwargs = {
+ key: cast(key, value)
+ for key, value in map(lambda l: l.split("="), lines)
+ if key in valid_fields
+ }
+ return cls(**kwargs)
+
+ def __repr__(self):
+ return "\n".join(f"{key}={value}" for key, value in dataclasses.asdict(self).items())
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/bad_plugin/BadPluginClass.java b/Ghidra/Features/Pyhidra/src/main/py/tests/data/bad_plugin/BadPluginClass.java
new file mode 100644
index 0000000000..92ddc11555
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/bad_plugin/BadPluginClass.java
@@ -0,0 +1,10 @@
+package ghidra.pyhidra.test;
+
+/**
+ * This is a bad class that will fail to compile.
+ *
+ * If a plugin fails to compile, it should only log a warning about it and continue.
+ */
+public class BadPluginClass extends Class {
+
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/example_script.py b/Ghidra/Features/Pyhidra/src/main/py/tests/data/example_script.py
new file mode 100644
index 0000000000..24e2600a92
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/example_script.py
@@ -0,0 +1,19 @@
+import sys
+
+
+def import_test_function():
+ print("imported successfully")
+
+
+if __name__ == '__main__':
+ print(" ".join(sys.argv))
+ print(" ".join(getScriptArgs()))
+ print(currentProgram)
+ assert currentProgram.name == "strings.exe"
+ assert currentProgram.listing
+ assert currentProgram.changeable
+ assert toAddr(0).offset == 0
+ assert monitor is not None
+ assert hasattr(__this__, "currentAddress")
+ assert currentSelection is None
+ assert currentHighlight is None
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/good_plugin/DummyTestRecognizer.java b/Ghidra/Features/Pyhidra/src/main/py/tests/data/good_plugin/DummyTestRecognizer.java
new file mode 100644
index 0000000000..399046c8ef
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/good_plugin/DummyTestRecognizer.java
@@ -0,0 +1,32 @@
+package ghidra.pyhidra.test;
+
+import ghidra.app.util.recognizer.Recognizer;
+
+/**
+ * Simple ExtensionPoint class for pyhidra plugin test.
+ *
+ * This can be any ExtensionPoint. Recognizer was chosen here
+ * because it has a small number of methods and hasn't changed in a long time.
+ */
+public class DummyTestRecognizer implements Recognizer {
+
+ // simple static field we can reach and check for a pytest
+ // normally this would be an interface implemented in Python
+ // that would be set so this class can call into Python
+ public static boolean preLaunchInitialized = false;
+
+ @Override
+ public int getPriority() {
+ return 0;
+ }
+
+ @Override
+ public int numberOfBytesRequired() {
+ return 0;
+ }
+
+ @Override
+ public String recognize(byte[] bytes) {
+ return "";
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/import_test_script.py b/Ghidra/Features/Pyhidra/src/main/py/tests/data/import_test_script.py
new file mode 100644
index 0000000000..786622a3b6
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/import_test_script.py
@@ -0,0 +1,4 @@
+from example_script import import_test_function
+
+if __name__ == '__main__':
+ import_test_function()
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/programless_script.py b/Ghidra/Features/Pyhidra/src/main/py/tests/data/programless_script.py
new file mode 100644
index 0000000000..791d6aa851
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/programless_script.py
@@ -0,0 +1,5 @@
+
+if __name__ == "__main__":
+ assert currentProgram is None
+ assert state.getProject() is not None
+ print("programless_script executed successfully")
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/data/projectless_script.py b/Ghidra/Features/Pyhidra/src/main/py/tests/data/projectless_script.py
new file mode 100644
index 0000000000..04087b65e2
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/data/projectless_script.py
@@ -0,0 +1,5 @@
+
+if __name__ == "__main__":
+ assert currentProgram is None
+ assert state.getProject() is None
+ print("projectless_script executed successfully")
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/test_argparser.py b/Ghidra/Features/Pyhidra/src/main/py/tests/test_argparser.py
new file mode 100644
index 0000000000..0ceb3290a8
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/test_argparser.py
@@ -0,0 +1,226 @@
+from pathlib import Path
+from typing import List, Tuple
+
+import pytest
+from pyhidra.__main__ import _get_parser, PyhidraArgs
+from pyhidra.ghidra_launch import ParsedArgs
+from pyhidra.ghidra_launch import get_parser as get_ghidra_launcher_parser
+
+
+PROJECT_NAME = "stub_name"
+EXE_NAME = "strings.exe"
+
+
+@pytest.fixture(autouse=True)
+def exe_file(shared_datadir: Path):
+ path = shared_datadir / EXE_NAME
+ path.touch()
+ yield path
+
+
+class TestArgParser:
+
+ def parse(self, *args) -> PyhidraArgs:
+ parser = _get_parser()
+ parser_args = PyhidraArgs(parser)
+ args = [str(arg) for arg in args]
+ parser.parse_args(args, namespace=parser_args)
+ return parser_args
+
+ @pytest.fixture(autouse=True)
+ def _test_root(self, shared_datadir: Path):
+ self.test_root = shared_datadir
+
+ @property
+ def example_script(self) -> Path:
+ return self.test_root / "example_script.py"
+
+ @property
+ def example_exe(self) -> Path:
+ return self.test_root / EXE_NAME
+
+ @property
+ def ghost_script(self) -> Path:
+ return self.test_root / "ghost_script.py"
+
+ @property
+ def ghost_exe(self) -> Path:
+ return self.test_root / "ghost.exe"
+
+ def test_no_args(self):
+ args = self.parse()
+ assert args.valid
+
+ def test_verbose_flag(self):
+ args = self.parse("-v")
+ assert args.verbose is True
+
+ def test_project_name(self):
+ args = self.parse("--project-name", PROJECT_NAME)
+ assert args.project_name == PROJECT_NAME
+ assert args.binary_path is None
+ assert args.script_path is None
+ assert args.project_path is None
+
+ def test_project_path(self):
+ args = self.parse("--project-path", self.test_root)
+ assert args.valid
+ assert args.project_path == self.test_root
+ assert args.binary_path is None
+ assert args.script_path is None
+ assert args.project_name is None
+
+ def test_script(self):
+ args = self.parse(self.example_script)
+ assert args.valid
+ assert args.script_path == self.example_script
+
+ def test_non_existing_script(self):
+ args = self.parse(self.ghost_script)
+ assert args.valid is False
+ assert args.script_path == self.ghost_script
+ assert args.binary_path is None
+
+ def test_binary(self):
+ args = self.parse(self.example_exe)
+ assert args.valid
+ assert args.binary_path == self.example_exe
+
+ def test_non_existing_binary(self):
+ args = self.parse(self.ghost_exe)
+ assert args.valid is False
+ assert args.binary_path == self.ghost_exe
+
+ def test_non_existing_binary_plus_script(self):
+ args = self.parse(self.ghost_exe, self.example_script)
+ assert args.valid is False
+ assert args.binary_path == self.ghost_exe
+ assert args.script_path == self.example_script
+
+ def test_script_with_non_existing_binary_arg(self):
+ args = self.parse(self.example_script, self.ghost_exe)
+ assert args.valid
+ assert args.binary_path is None
+ assert args.script_path == self.example_script
+ assert args.script_args == [str(self.ghost_exe)]
+
+ def test_script_with_optional_args(self):
+ args = self.parse(self.example_script, "--project-path", "-v", self.test_root)
+ assert args.valid
+ assert args.verbose is False
+ assert args.script_path == self.example_script
+ assert args.script_args == ["--project-path", "-v", str(self.test_root)]
+
+ def test_script_with_positional_args(self):
+ args = self.parse(
+ self.example_script,
+ self.test_root,
+ self.example_script,
+ self.ghost_script
+ )
+ assert args.valid
+ assert args.verbose is False
+ assert args.binary_path is None
+ assert args.script_path == self.example_script
+ script_args = [
+ str(arg) for arg in (self.test_root, self.example_script, self.ghost_script)
+ ]
+ assert args.script_args == script_args
+
+ def test_script_with_intermingled_args(self):
+ args = self.parse(
+ self.example_script,
+ self.example_exe,
+ "-v",
+ self.test_root,
+ "--project-path",
+ self.ghost_exe
+ )
+ assert args.valid
+ assert args.verbose is False
+ assert args.script_path == self.example_script
+ script_args = [
+ str(self.example_exe),
+ "-v", str(self.test_root),
+ "--project-path",
+ str(self.ghost_exe)
+ ]
+ assert args.script_args == script_args
+
+ def test_binary_script_with_intermingled_args(self):
+ args = self.parse(
+ "--project-name",
+ PROJECT_NAME,
+ self.example_exe,
+ self.example_script,
+ self.ghost_exe,
+ "-v",
+ self.test_root,
+ "--project-name",
+ self.ghost_exe
+ )
+ assert args.valid
+ assert args.verbose is False
+ assert args.project_name == PROJECT_NAME
+ assert args.binary_path == self.example_exe
+ assert args.script_path == self.example_script
+ script_args = [
+ str(self.ghost_exe),
+ "-v",
+ str(self.test_root),
+ "--project-name",
+ str(self.ghost_exe)
+ ]
+ assert args.script_args == script_args
+
+ def test_skip_analysis(self):
+ args = self.parse(
+ "--skip-analysis"
+ )
+ assert args.skip_analysis
+
+ def test_default_analysis(self):
+ args = self.parse()
+ assert not args.skip_analysis
+
+ def test_jvm_args(self):
+ ARG1 = "-Duser.variant="
+ ARG2 = "-Xmx1M"
+ args = self.parse(ARG1, ARG2)
+ jvm_args = args.jvm_args
+ assert jvm_args
+ assert ARG1 in jvm_args
+ assert ARG2 in jvm_args
+
+
+class TestGhidraLaunchParser:
+
+ def parse(self, *args) -> Tuple[ParsedArgs, str]:
+ parser = get_ghidra_launcher_parser()
+
+ parser_args = ParsedArgs()
+ _, remaining = parser.parse_known_args(args, namespace=parser_args)
+ return parser_args, remaining
+
+ def test_class_name(self):
+ CLASS_ARG = "ghidra.GhidraRun"
+ args, _ = self.parse("-g", CLASS_ARG, "arg1", "arg2", "--arg3", "value3")
+ assert args.class_name == CLASS_ARG
+
+ def test_gui_mode(self):
+ args, _ = self.parse("ghidra.GhidraRun", "arg1", "-g", "arg2", "--arg3", "value3")
+ assert args.gui
+
+ def test_jvm_args(self):
+ JVM_ARG1 = "-Duser.variant="
+ JVM_ARG2 = "-Xmx1M"
+ args, _ = self.parse("ghidra.GhidraRun", "arg1", JVM_ARG1, "arg2", "--arg3", "value3", JVM_ARG2)
+ jvm_args = args.jvm_args
+ assert jvm_args
+ assert JVM_ARG1 in jvm_args
+ assert JVM_ARG2 in jvm_args
+
+ def test_remaining(self):
+ _, remaining = self.parse("ghidra.GhidraRun", "arg1", "-Duser.variant=", "arg2", "--arg3", "value3", "-Xmx1M")
+ assert remaining
+ assert remaining == ["arg1", "arg2", "--arg3", "value3"]
\ No newline at end of file
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/test_core.py b/Ghidra/Features/Pyhidra/src/main/py/tests/test_core.py
new file mode 100644
index 0000000000..cdf5c5f1b2
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/test_core.py
@@ -0,0 +1,222 @@
+
+from pathlib import Path
+import textwrap
+import importlib
+import sys
+import jpype
+import pyhidra
+import pytest
+
+EXE_NAME = "strings.exe"
+TEST_LANGUAGE = "JVM:BE:32:default"
+TEST_COMPILER = "default"
+
+
+@pytest.fixture(autouse=True)
+def class_file(shared_datadir: Path):
+ path = shared_datadir / EXE_NAME
+ # creates a java class file of `public class Main {}`
+ path.write_bytes(bytes.fromhex("CAFEBABE00000041000A0A000200030700040C000500060100106A6176612F6C616E672F4F626A6563740100063C696E69743E0100032829560700080100044D61696E010004436F6465002100070002000000000001000100050006000100090000001100010001000000052AB70001B1000000000000"))
+ yield path
+
+
+def test_invalid_jpype_keyword_arg():
+ assert not jpype.isJVMStarted()
+
+ launcher = pyhidra.launcher.HeadlessPyhidraLauncher()
+ with pytest.raises(TypeError) as ex:
+ launcher.start(someBogusKeywordArg=True)
+ assert "startJVM() got an unexpected keyword argument 'someBogusKeywordArg'" in str(ex.value)
+
+
+def test_invalid_vm_arg_succeed():
+ assert not jpype.isJVMStarted()
+
+ launcher = pyhidra.launcher.HeadlessPyhidraLauncher()
+ launcher.add_vmargs('-XX:SomeBogusJvmArg')
+ launcher.start(ignoreUnrecognized=True)
+
+
+def test_run_script(capsys, shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ script_path = shared_datadir / "example_script.py"
+ pyhidra.run_script(strings_exe, script_path, script_args=["my", "--commands"], analyze=False)
+ captured = capsys.readouterr()
+
+ assert captured.err == ""
+
+ expected = textwrap.dedent(f"""\
+ {script_path} my --commands
+ my --commands
+ {EXE_NAME} - .ProgramDB
+ """)
+
+ assert captured.out == expected
+
+
+def test_open_program(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pyhidra.open_program(strings_exe, analyze=False, language=TEST_LANGUAGE, compiler=TEST_COMPILER) as flat_api:
+ assert flat_api.currentProgram.name == strings_exe.name
+ assert flat_api.getCurrentProgram().listing
+ assert flat_api.getCurrentProgram().changeable
+
+
+def test_bad_language(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pytest.raises(ValueError):
+ with pyhidra.open_program(
+ strings_exe,
+ analyze=False,
+ language="invalid"
+ ) as _:
+ pass
+
+
+def test_bad_compiler(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pytest.raises(ValueError):
+ with pyhidra.open_program(
+ strings_exe,
+ analyze=False,
+ language=TEST_LANGUAGE,
+ compiler="invalid"
+ ) as _:
+ pass
+
+
+def test_no_compiler(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pyhidra.open_program(strings_exe, analyze=False, language=TEST_LANGUAGE) as flat_api:
+ pass
+
+
+def test_no_language_with_compiler(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pyhidra.open_program(strings_exe, analyze=False, compiler=TEST_COMPILER) as flat_api:
+ pass
+
+
+def test_loader(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pyhidra.open_program(
+ strings_exe,
+ analyze=False,
+ language="DATA:LE:64:default",
+ compiler="pointer32",
+ loader="ghidra.app.util.opinion.BinaryLoader"
+ ) as flat_api:
+ assert bytes(flat_api.getBytes(flat_api.toAddr(0), 4)) == b"\xCA\xFE\xBA\xBE"
+
+
+def test_invalid_loader(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pytest.raises(ValueError):
+ with pyhidra.open_program(
+ strings_exe,
+ analyze=False,
+ language="DATA:LE:64:default",
+ compiler="pointer32",
+ loader="notaclass"
+ ) as _:
+ pass
+
+
+def test_invalid_loader_type(shared_datadir: Path):
+ strings_exe = shared_datadir / EXE_NAME
+ with pytest.raises(TypeError):
+ with pyhidra.open_program(
+ strings_exe,
+ analyze=False,
+ language="DATA:LE:64:default",
+ compiler="pointer32",
+ loader="ghidra.app.util.demangler.gnu.GnuDemangler"
+ ) as _:
+ pass
+
+
+def test_no_project(capsys, shared_datadir: Path):
+ pyhidra.run_script(None, shared_datadir / "projectless_script.py")
+ captured = capsys.readouterr()
+ assert captured.out.rstrip() == "projectless_script executed successfully"
+
+
+def test_no_program(capsys, shared_datadir: Path):
+ script_path = shared_datadir / "programless_script.py"
+ project_path = shared_datadir / "programless_ghidra"
+
+ pyhidra.run_script(None, script_path, project_path, "programless")
+ captured = capsys.readouterr()
+ assert captured.out.rstrip() == "programless_script executed successfully"
+
+
+def test_import_script(capsys, shared_datadir: Path):
+ script_path = shared_datadir / "import_test_script.py"
+ pyhidra.run_script(None, script_path)
+ captured = capsys.readouterr()
+ assert captured.out.rstrip() == "imported successfully"
+
+
+def test_import_ghidra_base_java_packages():
+
+ def get_runtime_top_level_java_packages(launcher) -> set:
+ from java.lang import Package
+
+ packages = set()
+
+ # Applicaiton needs to fully intialize to find all Ghidra packages
+ if launcher.has_launched():
+
+ for package in Package.getPackages():
+ # capture base packages only
+ packages.add(package.getName().split('.')[0])
+
+ return packages
+
+ def wrap_mod(mod):
+ return mod + '_'
+
+ launcher = pyhidra.start()
+
+ # Test to ensure _PyhidraImportLoader is last loader
+ assert isinstance(sys.meta_path[-1], pyhidra.launcher._PyhidraImportLoader)
+
+ packages = get_runtime_top_level_java_packages(launcher)
+
+ assert len(packages) > 0
+
+ # Test full coverage for Java base packages (_JImportLoader or _PyhidraImportLoader)
+ for mod in packages:
+ # check spec using standard import machinery "import mod"
+ spec = importlib.util.find_spec(mod)
+ if not isinstance(spec.loader, jpype.imports._JImportLoader):
+ # handle case with conflict. check spec with "import mod_"
+ spec = importlib.util.find_spec(wrap_mod(mod))
+
+ assert spec is not None
+ assert isinstance(spec.loader, jpype.imports._JImportLoader) or isinstance(
+ spec.loader, pyhidra.launcher._PyhidraImportLoader)
+
+ # Test all Java base packages are available with '_'
+ for mod in packages:
+ spec_ = importlib.util.find_spec(wrap_mod(mod))
+ assert spec_ is not None
+ assert isinstance(spec_.loader, pyhidra.launcher._PyhidraImportLoader)
+
+ # Test standard import
+ import ghidra
+ assert isinstance(ghidra.__loader__, jpype.imports._JImportLoader)
+
+ # Test import with conflict
+ import pdb_
+ assert isinstance(pdb_.__loader__, pyhidra.launcher._PyhidraImportLoader)
+
+ # Test "from" import with conflict
+ from pdb_ import PdbPlugin
+ from pdb_.symbolserver import LocalSymbolStore
+
+ # Test _Jpackage handles import that doesn't exist
+ try:
+ import pdb_.doesntexist
+ except ImportError:
+ pass
diff --git a/Ghidra/Features/Pyhidra/src/main/py/tests/test_plugin.py b/Ghidra/Features/Pyhidra/src/main/py/tests/test_plugin.py
new file mode 100644
index 0000000000..c030f651c1
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/main/py/tests/test_plugin.py
@@ -0,0 +1,167 @@
+import abc
+import functools
+import importlib.metadata
+from pathlib import Path
+import typing
+
+import jpype
+import pyhidra
+import pytest
+
+
+# mark this entire module
+pytestmark = pytest.mark.plugin
+
+
+SETUP_KEY = "pyhidra.setup"
+PRE_LAUNCH_KEY = "pyhidra.pre_launch"
+NAME_KEY = "names"
+
+
+plugin_registry: typing.Dict[str, typing.List["EntryPoint"]] = {
+ SETUP_KEY: [],
+ PRE_LAUNCH_KEY: [],
+ NAME_KEY: []
+}
+
+
+class PluginTest:
+
+ ran_setup = False
+ ran_prelaunch = False
+
+ details: pyhidra.ExtensionDetails = None
+
+ def __init_subclass__(cls) -> None:
+ cls.details = pyhidra.ExtensionDetails(
+ name=cls.__name__,
+ description="Test Plugin",
+ author=""
+ )
+
+ _setup = cls.setup
+ _prelaunch = cls.prelaunch
+
+ @functools.wraps(_setup)
+ def setup(launcher: pyhidra.HeadlessPyhidraLauncher):
+ _setup(launcher)
+ cls.ran_setup = True
+
+ @functools.wraps(_prelaunch)
+ def prelaunch():
+ _prelaunch()
+ cls.ran_prelaunch = True
+
+ cls.setup = setup
+ cls.prelaunch = prelaunch
+
+ name = cls.__name__
+ plugin_registry[SETUP_KEY].append(EntryPoint(name, cls.setup))
+ plugin_registry[PRE_LAUNCH_KEY].append(EntryPoint(name, cls.prelaunch))
+ plugin_registry[NAME_KEY].append(name)
+
+ @classmethod
+ @abc.abstractmethod
+ def setup(cls, launcher: pyhidra.HeadlessPyhidraLauncher):
+ ...
+
+ @classmethod
+ @abc.abstractmethod
+ def prelaunch(cls):
+ ...
+
+ @classmethod
+ def test_setup(cls):
+ assert cls.ran_setup
+
+ @classmethod
+ def test_prelaunch(cls):
+ assert cls.ran_prelaunch
+
+
+class EntryPoint:
+
+ def __init__(self, name, callback):
+ self.name = name
+ self.callback = callback
+
+ def load(self):
+ return self.callback
+
+
+def _monkey_patch_entry_points():
+ # hardcode the entry points so we don't need to pip install anything
+ backup = importlib.metadata.entry_points
+
+ def entry_points(*args, **kwargs):
+ group = kwargs.get("group")
+ if group in plugin_registry:
+ return plugin_registry[group]
+ return backup(*args, **kwargs)
+
+ importlib.metadata.entry_points = entry_points
+
+
+@pytest.fixture(scope="module", autouse=True)
+def with_ghidra():
+ """
+ Automatically used fixture that starts Ghidra,
+ yields nothing and then cleans up the test plugins
+ """
+ _monkey_patch_entry_points()
+ try:
+ launcher = pyhidra.HeadlessPyhidraLauncher()
+ launcher.start()
+ yield # can't yield None
+ finally:
+ # we need to close the GhidraClassLoader so we can delete the extension
+ from java.lang import ClassLoader
+ ClassLoader.getSystemClassLoader().close()
+ jpype.shutdownJVM()
+ for plugin in plugin_registry["names"]:
+ try:
+ launcher.uninstall_plugin(plugin)
+ except Exception:
+ pass
+
+
+class TestValidPlugin(PluginTest):
+
+ @classmethod
+ def setup(cls, launcher: pyhidra.HeadlessPyhidraLauncher):
+ source_path = Path(__file__).parent / "data" / "good_plugin"
+ launcher.install_plugin(source_path, cls.details)
+
+ @classmethod
+ def prelaunch(cls):
+ DummyTestRecognizer = jpype.JClass("ghidra.pyhidra.test.DummyTestRecognizer")
+ DummyTestRecognizer.preLaunchInitialized = True
+
+ @classmethod
+ def test_extension_point(cls):
+ from ghidra.app.util.recognizer import Recognizer
+ from ghidra.util.classfinder import ClassSearcher
+ DummyTestRecognizer = jpype.JClass("ghidra.pyhidra.test.DummyTestRecognizer")
+ assert DummyTestRecognizer in ClassSearcher.getClasses(Recognizer)
+
+
+class TestBadPlugin(PluginTest):
+
+ launcher: pyhidra.HeadlessPyhidraLauncher = None
+
+ @classmethod
+ def setup(cls, launcher: pyhidra.HeadlessPyhidraLauncher):
+ source_path = Path(__file__).parent / "data" / "bad_plugin"
+ launcher.install_plugin(source_path, cls.details)
+ cls.launcher = launcher
+
+ @classmethod
+ def prelaunch(cls):
+ pass
+
+ @classmethod
+ def test_no_plugin(cls):
+ # ensures there is no plugin
+ assert cls.launcher
+ extension_path = cls.launcher.extension_path
+ assert not (extension_path / cls.__name__).exists()
diff --git a/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PyhidraPluginTest.java b/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PyhidraPluginTest.java
new file mode 100644
index 0000000000..139a81cdcc
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PyhidraPluginTest.java
@@ -0,0 +1,49 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.pyhidra;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+import ghidra.app.plugin.core.osgi.BundleHost;
+import ghidra.app.script.GhidraScriptUtil;
+import ghidra.framework.plugintool.PluginTool;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+import ghidra.test.TestEnv;
+
+/**
+ * Tests the Python Plugin functionality.
+ */
+public class PyhidraPluginTest extends AbstractGhidraHeadedIntegrationTest {
+
+ private TestEnv env;
+
+ @Before
+ public void setUp() throws Exception {
+ env = new TestEnv();
+ PluginTool tool = env.getTool();
+ GhidraScriptUtil.initialize(new BundleHost(), null);
+ tool.addPlugin(PyhidraPlugin.class.getName());
+ env.getPlugin(PyhidraPlugin.class);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ GhidraScriptUtil.dispose();
+ env.dispose();
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PythonScriptInfoTest.java b/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PythonScriptInfoTest.java
new file mode 100644
index 0000000000..d6b6608156
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/test.slow/java/ghidra/pyhidra/PythonScriptInfoTest.java
@@ -0,0 +1,237 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.pyhidra;
+
+import static org.junit.Assert.*;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.swing.KeyStroke;
+
+import org.junit.*;
+
+import generic.jar.ResourceFile;
+import ghidra.app.plugin.core.osgi.BundleHost;
+import ghidra.app.script.GhidraScriptUtil;
+import ghidra.app.script.ScriptInfo;
+import ghidra.test.AbstractGhidraHeadedIntegrationTest;
+
+public class PythonScriptInfoTest extends AbstractGhidraHeadedIntegrationTest {
+
+ @Before
+ public void setUp() throws Exception {
+ GhidraScriptUtil.initialize(new BundleHost(), null);
+ Path userScriptDir = java.nio.file.Paths.get(GhidraScriptUtil.USER_SCRIPTS_DIR);
+ if (Files.notExists(userScriptDir)) {
+ Files.createDirectories(userScriptDir);
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ GhidraScriptUtil.dispose();
+ }
+
+ @Test
+ public void testDetailedPythonScript() {
+ String descLine1 = "This script exists to check that the info on";
+ String descLine2 = "a script that has extensive documentation is";
+ String descLine3 = "properly parsed and represented.";
+ String author = "Fake Name";
+ String categoryTop = "Test";
+ String categoryBottom = "ScriptInfo";
+ String keybinding = "ctrl shift COMMA";
+ String menupath = "File.Run.Detailed Script";
+ String importPackage = "detailStuff";
+ ResourceFile scriptFile = null;
+
+ try {
+ //@formatter:off
+ scriptFile = createTempPyScriptFileWithLines(
+ "'''",
+ "This is a test block comment. It will be ignored.",
+ "@category NotTheRealCategory",
+ "'''",
+ "#" + descLine1,
+ "#" + descLine2,
+ "#" + descLine3,
+ "#@author " + author,
+ "#@category " + categoryTop + "." + categoryBottom,
+ "#@keybinding " + keybinding,
+ "#@menupath " + menupath,
+ "#@importpackage " + importPackage,
+ "print('for a blank class, it sure is well documented!')");
+ //@formatter:on
+ }
+ catch (IOException e) {
+ fail("couldn't create a test script: " + e.getMessage());
+ }
+
+ ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
+
+ String expectedDescription = descLine1 + " \n" + descLine2 + " \n" + descLine3 + " \n";
+ assertEquals(expectedDescription, info.getDescription());
+
+ assertEquals(author, info.getAuthor());
+ assertEquals(KeyStroke.getKeyStroke(keybinding), info.getKeyBinding());
+ assertEquals(menupath.replace(".", "->"), info.getMenuPathAsString());
+ assertEquals(importPackage, info.getImportPackage());
+
+ String[] actualCategory = info.getCategory();
+ assertEquals(2, actualCategory.length);
+ assertEquals(categoryTop, actualCategory[0]);
+ assertEquals(categoryBottom, actualCategory[1]);
+ }
+
+ @Test
+ public void testPythonScriptWithBlockComment() {
+ String description = "Script with a block comment at the top.";
+ String category = "Test";
+ ResourceFile scriptFile = null;
+
+ try {
+ //@formatter:off
+ scriptFile = createTempPyScriptFileWithLines(
+ "'''",
+ "This is a test block comment. It will be ignored.",
+ "@category NotTheRealCategory",
+ "'''",
+ "#" + description,
+ "#@category " + category,
+ "print 'hello!'");
+ //@formatter:on
+ }
+ catch (IOException e) {
+ fail("couldn't create a test script: " + e.getMessage());
+ }
+
+ ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
+ assertEquals(description + " \n", info.getDescription());
+
+ String[] actualCategory = info.getCategory();
+ assertEquals(1, actualCategory.length);
+ assertEquals(category, actualCategory[0]);
+ }
+
+ @Test
+ public void testPythonScriptWithBlockCommentAndCertifyHeader() {
+ String description = "Script with a block comment at the top.";
+ String category = "Test";
+ ResourceFile scriptFile = null;
+
+ try {
+ //@formatter:off
+ scriptFile = createTempPyScriptFileWithLines(
+ "## ###",
+ "# IP: GHIDRA",
+ "# ",
+ "# Some license text...",
+ "# you may not use this file except in compliance with the License.",
+ "# ",
+ "# blah blah blah",
+ "##",
+ "",
+ "'''",
+ "This is a test block comment. It will be ignored.",
+ "@category NotTheRealCategory",
+ "'''",
+ "#" + description,
+ "#@category " + category,
+ "print 'hello!'");
+ //@formatter:on
+ }
+ catch (IOException e) {
+ fail("couldn't create a test script: " + e.getMessage());
+ }
+
+ ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
+ assertEquals(description + " \n", info.getDescription());
+
+ String[] actualCategory = info.getCategory();
+ assertEquals(1, actualCategory.length);
+ assertEquals(category, actualCategory[0]);
+ }
+
+ @Test
+ public void testPythonScriptWithoutBlockComment() {
+ String description = "Script without a block comment at the top.";
+ String category = "Test";
+ ResourceFile scriptFile = null;
+
+ try {
+ //@formatter:off
+ scriptFile = createTempPyScriptFileWithLines(
+ "#" + description,
+ "#@category " + category,
+ "print 'hello!'");
+ //@formatter:on
+ }
+ catch (IOException e) {
+ fail("couldn't create a test script: " + e.getMessage());
+ }
+
+ ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
+ assertEquals(description + " \n", info.getDescription());
+
+ String[] actualCategory = info.getCategory();
+ assertEquals(1, actualCategory.length);
+ assertEquals(category, actualCategory[0]);
+ }
+
+ @Test
+ public void testPythonScriptWithSingleLineBlockComment() {
+ String description = "Script with a block comment at the top.";
+ String category = "Test";
+ ResourceFile scriptFile = null;
+
+ try {
+ //@formatter:off
+ scriptFile = createTempPyScriptFileWithLines(
+ "'''This is a test block comment. It will be ignored.'''",
+ "#" + description,
+ "#@category " + category,
+ "print 'hello!'");
+ //@formatter:on
+ }
+ catch (IOException e) {
+ fail("couldn't create a test script: " + e.getMessage());
+ }
+
+ ScriptInfo info = GhidraScriptUtil.newScriptInfo(scriptFile);
+ assertEquals(description + " \n", info.getDescription());
+
+ String[] actualCategory = info.getCategory();
+ assertEquals(1, actualCategory.length);
+ assertEquals(category, actualCategory[0]);
+ }
+
+ private ResourceFile createTempPyScriptFileWithLines(String... lines) throws IOException {
+ File scriptDir = new File(GhidraScriptUtil.USER_SCRIPTS_DIR);
+ File tempFile = File.createTempFile(testName.getMethodName(), ".py", scriptDir);
+ tempFile.deleteOnExit();
+ ResourceFile tempResourceFile = new ResourceFile(tempFile);
+
+ PrintWriter writer = new PrintWriter(tempResourceFile.getOutputStream());
+ for (String line : lines) {
+ writer.println(line);
+ }
+ writer.close();
+
+ return tempResourceFile;
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/PythonFieldExposerTest.java b/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/PythonFieldExposerTest.java
new file mode 100644
index 0000000000..7b05a33aac
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/PythonFieldExposerTest.java
@@ -0,0 +1,18 @@
+package ghidra.pyhidra;
+
+import org.junit.Test;
+
+import ghidra.pyhidra.PythonFieldExposer.ExposedField;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;;
+
+public class PythonFieldExposerTest {
+
+ @Test
+ public void test() {
+ Map fields = PythonFieldExposer.getProperties(PyhidraScriptProvider.PyhidraGhidraScript.class);
+ assertTrue(fields.containsKey("currentProgram"));
+ }
+}
diff --git a/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/property/PropertyUtilsTest.java b/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/property/PropertyUtilsTest.java
new file mode 100644
index 0000000000..8349a741ef
--- /dev/null
+++ b/Ghidra/Features/Pyhidra/src/test/java/ghidra/pyhidra/property/PropertyUtilsTest.java
@@ -0,0 +1,240 @@
+package ghidra.pyhidra.property;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static org.junit.Assert.assertArrayEquals;
+
+@RunWith(Parameterized.class)
+public class PropertyUtilsTest {
+
+ @Parameters(name = "{0}")
+ public static List data() {
+ return convertData(PropertyUtilsTest.class.getNestMembers());
+ }
+
+ private final Class> cls;
+
+ public PropertyUtilsTest(String name, Class> cls) {
+ this.cls = cls;
+ }
+
+ private TestResult[] getExpected() {
+ return Arrays.stream(cls.getAnnotationsByType(ExpectedResult.class))
+ .map(TestResult::new)
+ .toArray(TestResult[]::new);
+ }
+
+ @Test
+ public void test() {
+ TestResult[] expected = getExpected();
+ TestResult[] properties = getProperties(cls);
+ assertArrayEquals(expected, properties);
+ }
+
+ private static TestResult[] getProperties(Class> cls) {
+ return Arrays.stream(PropertyUtils.getProperties(cls))
+ .map(AbstractJavaProperty.class::cast)
+ .map(TestResult::new)
+ .toArray(TestResult[]::new);
+ }
+
+ private static List convertData(Class>[] classes) {
+ List result = new ArrayList<>(classes.length);
+ for (Class> cls : classes) {
+ if (cls.isRecord() || cls.isAnnotation()) {
+ continue;
+ }
+ result.add(new Object[] { cls.getSimpleName(), cls });
+ }
+ return result;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @Repeatable(ExpectedResults.class)
+ private static @interface ExpectedResult {
+ String field();
+
+ boolean getter();
+
+ boolean setter();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ private static @interface ExpectedResults {
+ ExpectedResult[] value();
+ }
+
+ private static record TestResult(String field, boolean getter, boolean setter) {
+ TestResult(AbstractJavaProperty> property) {
+ this(property.field, property.hasGetter(), property.hasValidSetter());
+ }
+
+ TestResult(ExpectedResult result) {
+ this(result.field(), result.getter(), result.setter());
+ }
+ }
+
+ @ExpectedResult(field = "length", getter = true, setter = false)
+ public static class TestGetter {
+ public int getLength() {
+ return 0;
+ }
+ }
+
+ @ExpectedResult(field = "length", getter = false, setter = true)
+ public static class TestSetter {
+ public void setLength(int i) {
+ }
+ }
+
+ @ExpectedResult(field = "length", getter = true, setter = true)
+ public static class TestProperty {
+ public int getLength() {
+ return 0;
+ }
+
+ public void setLength(int i) {
+ }
+ }
+
+ @ExpectedResult(field = "length", getter = true, setter = true)
+ public static class TestMultiSetter {
+ public int getLength() {
+ return 0;
+ }
+
+ public void setLength(int i) {
+ }
+
+ public void setLength(short s) {
+ }
+ }
+
+ @ExpectedResult(field = "length", getter = true, setter = true)
+ public static class TestBoxedMultiSetter {
+ public int getLength() {
+ return 0;
+ }
+
+ public void setLength(int i) {
+ }
+
+ public void setLength(Integer i) {
+ }
+ }
+
+ public static class TestMultiSetterNoGetter {
+ public void setLength(int i) {
+ }
+
+ public void setLength(short s) {
+ }
+ }
+
+ @ExpectedResult(field = "valid", getter = true, setter = false)
+ public static class TestIsGetter {
+ public boolean isValid() {
+ return true;
+ }
+ }
+
+ @ExpectedResult(field = "valid", getter = true, setter = true)
+ public static class TestIsProperty {
+ public boolean isValid() {
+ return true;
+ }
+
+ public void setValid(boolean valid) {
+ }
+ }
+
+ @ExpectedResult(field = "valid", getter = true, setter = false)
+ public static class TestIsBoxedGetter {
+ public Boolean isValid() {
+ return true;
+ }
+ }
+
+ @ExpectedResult(field = "valid", getter = true, setter = true)
+ public static class TestIsBoxedProperty {
+ public Boolean isValid() {
+ return true;
+ }
+
+ public void setValid(boolean valid) {
+ }
+ }
+
+ public static class TestBadIsGetter {
+ public int isValid() {
+ return 1;
+ }
+ }
+
+ public static class TestIsGetterName {
+ public boolean isvalid() {
+ return true;
+ }
+ }
+
+ public static class TestBadGetterName {
+ public int getlength() {
+ return 0;
+ }
+ }
+
+ public static class TestBadSetterName {
+ public void setlength(int i) {
+ }
+ }
+
+ public static class TestBadIsTooShortName {
+ public boolean i() {
+ return true;
+ }
+ }
+
+ public static class TestBadGetTooShortName {
+ public int ge() {
+ return 0;
+ }
+ }
+
+ public static class TestBadSetTooShortName {
+ public int se() {
+ return 0;
+ }
+ }
+
+ public static class TestBadIsNoName {
+ public boolean is() {
+ return true;
+ }
+ }
+
+ public static class TestBadGetNoName {
+ public int get() {
+ return 0;
+ }
+ }
+
+ public static class TestBadSetNoName {
+ public int set() {
+ return 0;
+ }
+ }
+}
diff --git a/Ghidra/RuntimeScripts/Linux/support/pythonRun b/Ghidra/RuntimeScripts/Linux/support/jythonRun
old mode 100755
new mode 100644
similarity index 100%
rename from Ghidra/RuntimeScripts/Linux/support/pythonRun
rename to Ghidra/RuntimeScripts/Linux/support/jythonRun
diff --git a/Ghidra/RuntimeScripts/Linux/support/pyhidraRun b/Ghidra/RuntimeScripts/Linux/support/pyhidraRun
new file mode 100644
index 0000000000..cc508ee15c
--- /dev/null
+++ b/Ghidra/RuntimeScripts/Linux/support/pyhidraRun
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+#----------------------------------------
+# Pyhidra launch
+#----------------------------------------
+
+# Resolve symbolic link if present and get the directory this script lives in.
+# NOTE: "readlink -f" is best but works on Linux only, "readlink" will only work if your PWD
+# contains the link you are calling (which is the best we can do on macOS), and the "echo" is the
+# fallback, which doesn't attempt to do anything with links.
+SCRIPT_FILE="$(readlink -f "$0" 2>/dev/null || readlink "$0" 2>/dev/null || echo "$0")"
+SCRIPT_DIR="${SCRIPT_FILE%/*}"
+
+# Add optional JVM args inside the quotes
+VMARG_LIST=""
+
+# Make sure Python3 is installed
+if ! [ -x "$(command -v python3)" ] ; then
+ echo "Python 3 is not installed."
+ exit 1
+fi
+
+# Dev mode or production mode?
+DEV_ARG=
+INSTALL_DIR="${SCRIPT_DIR}/.."
+if [ ! -d "${INSTALL_DIR}/Ghidra" ]; then
+ DEV_ARG="--dev"
+ INSTALL_DIR="${SCRIPT_DIR}/../../../.."
+fi
+
+PYHIDRA_LAUNCHER="${INSTALL_DIR}/Ghidra/Features/Pyhidra/pyhidraLauncher.py"
+
+python3 "${PYHIDRA_LAUNCHER}" "${INSTALL_DIR}" ${DEV_ARG} ${VMARG_LIST} "$@"
diff --git a/Ghidra/RuntimeScripts/Windows/support/pythonRun.bat b/Ghidra/RuntimeScripts/Windows/support/jythonRun.bat
similarity index 100%
rename from Ghidra/RuntimeScripts/Windows/support/pythonRun.bat
rename to Ghidra/RuntimeScripts/Windows/support/jythonRun.bat
diff --git a/Ghidra/RuntimeScripts/Windows/support/pyhidraRun.bat b/Ghidra/RuntimeScripts/Windows/support/pyhidraRun.bat
new file mode 100644
index 0000000000..19c0d91554
--- /dev/null
+++ b/Ghidra/RuntimeScripts/Windows/support/pyhidraRun.bat
@@ -0,0 +1,46 @@
+:: Pyhidra launch
+
+@echo off
+setlocal enabledelayedexpansion
+
+:: See if we were doubled clicked or run from a command prompt
+set DOUBLE_CLICKED=n
+for /f "tokens=2" %%# in ("%cmdcmdline%") do if /i "%%#" equ "/c" set DOUBLE_CLICKED=y
+
+:: Add optional JVM args inside the quotes
+set VMARG_LIST=-Dsun.java2d.dpiaware=true
+
+:: Make sure Python3 is installed
+set PYTHON=py
+where /q %PYTHON%
+if not %ERRORLEVEL% == 0 (
+ set PYTHON=python
+ where /q !PYTHON!
+ if not !ERRORLEVEL! == 0 (
+ echo Python 3 is not installed.
+ goto exit1
+ )
+)
+
+:: Dev mode or production mode?
+set DEV_ARG=
+set "SCRIPT_DIR=%~dp0"
+set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
+set "INSTALL_DIR=%SCRIPT_DIR%\.."
+if not exist "%INSTALL_DIR%\Ghidra" (
+ set DEV_ARG="--dev"
+ set "INSTALL_DIR=%SCRIPT_DIR%\..\..\..\.."
+)
+
+set "PYHIDRA_LAUNCHER=%INSTALL_DIR%\Ghidra\Features\Pyhidra\pyhidraLauncher.py
+
+%PYTHON% "%PYHIDRA_LAUNCHER%" "%INSTALL_DIR%" %DEV_ARG% %VMARG_LIST% %*
+
+:exit1
+if not %ERRORLEVEL% == 0 (
+ if "%DOUBLE_CLICKED%"=="y" (
+ pause
+ )
+)
+
+exit /B %ERRORLEVEL%
diff --git a/Ghidra/RuntimeScripts/build.gradle b/Ghidra/RuntimeScripts/build.gradle
index cc665f5b3b..50504dabfc 100644
--- a/Ghidra/RuntimeScripts/build.gradle
+++ b/Ghidra/RuntimeScripts/build.gradle
@@ -43,7 +43,7 @@ rootProject.PLATFORMS.each { platform ->
include "gradlew"
into "support/gradle"
}
- t.from (p.file("Linux/ghidraRun"))
+ t.from (p.file("Linux/ghidraRun"))
}
if (isWindows(platform.name)) {
diff --git a/GhidraBuild/BuildFiles/Doclets/build.gradle b/GhidraBuild/BuildFiles/Doclets/build.gradle
index 7f5cc31b4a..8115975f7f 100644
--- a/GhidraBuild/BuildFiles/Doclets/build.gradle
+++ b/GhidraBuild/BuildFiles/Doclets/build.gradle
@@ -28,4 +28,5 @@ dependencies {
}
rootProject.createJsondocs.dependsOn jar
+rootProject.createPythonTypeStubs.dependsOn jar
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/DocConverter.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/DocConverter.java
new file mode 100644
index 0000000000..d59d348893
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/DocConverter.java
@@ -0,0 +1,156 @@
+package ghidra.doclets.typestubs;
+
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.util.Elements;
+import javax.tools.Diagnostic;
+
+import com.sun.source.doctree.AttributeTree;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.util.DocTreePath;
+import com.sun.source.util.DocTrees;
+import com.sun.source.util.TreePath;
+
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * Base class for recursively converting documentation
+ */
+abstract class DocConverter {
+
+ static final int INDENT_WIDTH = 4;
+
+ private final DocletEnvironment env;
+ private final Reporter log;
+
+ /**
+ * Creates a new {@link DocConverter}
+ *
+ * @param env the doclet environment
+ * @param log the log
+ */
+ DocConverter(DocletEnvironment env, Reporter log) {
+ this.env = env;
+ this.log = log;
+ }
+
+ /**
+ * Converts the provided Javadoc tag
+ *
+ * @param el the current element
+ * @param tag the Javadoc tag
+ * @return the converted tag
+ */
+ abstract String convertTag(Element el, DocTree tag, ListIterator extends DocTree> it);
+
+ /**
+ * Converts the provided doc tree
+ *
+ * @param el the current element
+ * @param tree the doc tree
+ * @return the converted doc tree
+ */
+ public String convertTree(Element el, List extends DocTree> tree) {
+ StringBuilder builder = new StringBuilder();
+ ListIterator extends DocTree> it = tree.listIterator();
+ while (it.hasNext()) {
+ builder.append(convertTag(el, it.next(), it));
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Logs a warning with the provided message
+ *
+ * @param el the current element
+ * @param tag the current tag
+ * @param message the message
+ */
+ final void logWarning(Element el, DocTree tag, String message) {
+ try {
+ DocCommentTree tree = env.getDocTrees().getDocCommentTree(el);
+ TreePath treePath = env.getDocTrees().getPath(el);
+ DocTreePath path = DocTreePath.getPath(treePath, tree, tag);
+ if (path != null) {
+ log.print(Diagnostic.Kind.WARNING, path, message);
+ }
+ else {
+ log.print(Diagnostic.Kind.WARNING, el, message);
+ }
+ }
+ catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ /**
+ * Logs an error with the provided message
+ *
+ * @param el the current element
+ * @param tag the current tag
+ * @param message the message
+ */
+ final void logError(Element el, DocTree tag, String message) {
+ try {
+ DocCommentTree tree = env.getDocTrees().getDocCommentTree(el);
+ TreePath treePath = env.getDocTrees().getPath(el);
+ DocTreePath path = DocTreePath.getPath(treePath, tree, tag);
+ if (path != null) {
+ log.print(Diagnostic.Kind.ERROR, path, message);
+ }
+ else {
+ log.print(Diagnostic.Kind.ERROR, el, message);
+ }
+ }
+ catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ final DocTrees getDocTrees() {
+ return env.getDocTrees();
+ }
+
+ final Elements getElementUtils() {
+ return env.getElementUtils();
+ }
+
+ /**
+ * Gets a mapping of the provided list of attributes
+ *
+ * @param attributes the attributes list
+ * @return the attributes mapping
+ */
+ Map getAttributes(Element el, List extends DocTree> attributes) {
+ return attributes
+ .stream()
+ .filter(AttributeTree.class::isInstance)
+ .map(AttributeTree.class::cast)
+ .collect(Collectors.toMap(attr -> attr.getName().toString().toLowerCase(),
+ attr -> attr.getValue() != null ? convertTree(el, attr.getValue()) : ""));
+ }
+
+ /**
+ * Aligns the lines in the provided text to the same indentation level
+ *
+ * @param text the text
+ * @return the new text all aligned to the same indentation level
+ */
+ static String alignIndent(String text) {
+ int index = text.indexOf('\n');
+ if (index == -1) {
+ return text;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ return builder.append(text.substring(0, index + 1))
+ .append(text.substring(index + 1).stripIndent())
+ .toString();
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/GhidraBuiltinsBuilder.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/GhidraBuiltinsBuilder.java
new file mode 100644
index 0000000000..21bd1190ba
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/GhidraBuiltinsBuilder.java
@@ -0,0 +1,220 @@
+package ghidra.doclets.typestubs;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.Elements;
+
+/**
+ * A builder class for the pseudo ghidra.ghidra_builtins package
+ */
+class GhidraBuiltinsBuilder {
+
+ private static final String INDENT = "";
+
+ private final PythonTypeStubDoclet doclet;
+ private final PythonTypeStubType api;
+ private final PythonTypeStubType script;
+
+ /**
+ * Creates a new {@link GhidraBuiltinsBuilder}
+ *
+ * @param doclet the current doclet
+ */
+ GhidraBuiltinsBuilder(PythonTypeStubDoclet doclet) {
+ this.doclet = doclet;
+ this.api = getType(doclet, "ghidra.program.flatapi.FlatProgramAPI");
+ this.script = getType(doclet, "ghidra.app.script.GhidraScript");
+ }
+
+ /**
+ * Processes the pseudo ghidra.ghidra_builtins package
+ */
+ void process() {
+ File root = new File(doclet.getDestDir(), "ghidra-stubs/ghidra_builtins");
+ root.mkdirs();
+ File stub = new File(root, "__init__.pyi");
+ try (PrintWriter printer = new PrintWriter(new FileWriter(stub))) {
+ process(printer);
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Processes the pseudo ghidra.ghidra_builtins package using the provided printer
+ *
+ * @param printer the printer
+ */
+ private void process(PrintWriter printer) {
+ // collect methods and fields early to ensure protected visibility
+ api.getMethods(true, true);
+ script.getMethods(true, true);
+ api.getFields(true);
+ script.getFields(true);
+
+ script.writeJavaDoc(printer, INDENT);
+ printer.println();
+
+ printScriptImports(printer);
+ printTypeVars(printer);
+
+ // we need to keep track of things to export for __all__
+ Set exports = new LinkedHashSet<>();
+
+ printFields(printer, exports);
+
+ printer.println();
+ printer.println();
+
+ printMethods(printer, exports);
+
+ printer.print("__all__ = [");
+ printer.print(String.join(", ", exports));
+ printer.println("]");
+ }
+
+ /**
+ * Prints all necessary TypeVars
+ *
+ * @param printer the printer
+ */
+ private void printTypeVars(PrintWriter printer) {
+ for (String typevar : getScriptTypeVars()) {
+ printer.print(typevar);
+ printer.print(" = typing.TypeVar(\"");
+ printer.print(typevar);
+ printer.println("\")");
+ }
+ printer.println();
+ printer.println();
+ }
+
+ /**
+ * Prints all the script fields
+ *
+ * @param printer the printer
+ * @param exports the set of fields to export
+ */
+ private void printFields(PrintWriter printer, Set exports) {
+ // always use false for static so typing.ClassVar is not emitted
+ for (VariableElement field : api.getFields(true)) {
+ api.printField(field, printer, INDENT, false);
+ exports.add('"' + field.getSimpleName().toString() + '"');
+ }
+ for (VariableElement field : script.getFields(true)) {
+ script.printField(field, printer, INDENT, false);
+ exports.add('"' + field.getSimpleName().toString() + '"');
+ }
+ }
+
+ /**
+ * Prints all the script methods
+ *
+ * @param printer the printer
+ * @param exports the set of methods to export
+ */
+ private void printMethods(PrintWriter printer, Set exports) {
+ // methods must be sorted by name for typing.overload
+ List apiMethods = api.getMethods(true, true);
+ List scriptMethods = script.getMethods(true, true);
+
+ int length = apiMethods.size() + scriptMethods.size();
+ List methods = new ArrayList<>(length);
+
+ methods.addAll(apiMethods);
+ methods.addAll(scriptMethods);
+ methods.sort(null);
+
+ ListIterator methodIterator = methods.listIterator();
+
+ while (methodIterator.hasNext()) {
+ PythonTypeStubMethod method = methodIterator.next();
+ boolean overload = PythonTypeStubType.isOverload(methods, methodIterator, method);
+ method.process(printer, INDENT, overload);
+ exports.add('"' + method.getName() + '"');
+ printer.println();
+ }
+ }
+
+ /**
+ * Gets a list of all imported packages
+ *
+ * @return the list of packages
+ */
+ private List getScriptPackages() {
+ Set packages = new HashSet<>();
+ for (TypeElement type : api.getImportedTypes()) {
+ packages.add(PythonTypeStubElement.getPackage(type));
+ }
+ for (TypeElement type : script.getImportedTypes()) {
+ packages.add(PythonTypeStubElement.getPackage(type));
+ }
+ List res = new ArrayList<>(packages);
+ res.sort(PythonTypeStubElement::compareQualifiedNameable);
+ return res;
+ }
+
+ /**
+ * Prints the imports needed by this package
+ *
+ * @param printer the printer
+ */
+ private void printScriptImports(PrintWriter printer) {
+ printer.println("import collections.abc");
+ printer.println("import typing");
+ printer.println("from warnings import deprecated # type: ignore");
+ printer.println();
+ printer.println("import jpype # type: ignore");
+ printer.println("import jpype.protocol # type: ignore");
+ printer.println();
+ doclet.printImports(printer, getScriptPackages());
+ printer.println();
+ printer.println();
+ printer.println("from ghidra.app.script import *");
+ printer.println();
+ printer.println();
+ }
+
+ /**
+ * Gets a list of TypeVars needed by this package
+ *
+ * @return the list of TypeVars
+ */
+ private List getScriptTypeVars() {
+ // all this for only two typing.TypeVar
+ // at least this is future proof
+ Set vars = new HashSet<>(api.getTypeVars());
+ vars.addAll(script.getTypeVars());
+
+ List res = new ArrayList<>(vars);
+ res.sort(null);
+ return res;
+ }
+
+ /**
+ * Gets the PythonTypeStubType for the provided type name
+ *
+ * @param doclet the current doclet
+ * @param name the type name
+ * @return the requested type
+ */
+ private static PythonTypeStubType getType(PythonTypeStubDoclet doclet, String name) {
+ Elements elements = doclet.getElementUtils();
+ TypeElement type = elements.getTypeElement(name);
+ PackageElement pkg = (PackageElement) type.getEnclosingElement();
+ return new PythonTypeStubType(new PythonTypeStubPackage(doclet, pkg), type);
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlConverter.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlConverter.java
new file mode 100644
index 0000000000..a491ff41ef
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlConverter.java
@@ -0,0 +1,494 @@
+package ghidra.doclets.typestubs;
+
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+import javax.lang.model.element.Element;
+
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.TextTree;
+
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * Helper class for converting HTML to reStructuredText
+ */
+public final class HtmlConverter extends DocConverter {
+
+ private final JavadocConverter docConverter;
+
+ /**
+ * Creates a new {@link HtmlConverter}
+ *
+ * @param env the doclet environment
+ * @param log the log
+ */
+ public HtmlConverter(DocletEnvironment env, Reporter log, JavadocConverter docConverter) {
+ super(env, log);
+ this.docConverter = docConverter;
+ }
+
+ @Override
+ String convertTag(Element el, DocTree tag, ListIterator extends DocTree> it) {
+ return docConverter.convertTag(el, tag, it);
+ }
+
+ /**
+ * Gets a map of the attributes in the html element
+ *
+ * @param start the start element
+ * @return the attributes map
+ */
+ public Map getAttributes(Element el, StartElementTree start) {
+ return getAttributes(el, start.getAttributes());
+ }
+
+ /**
+ * Logs a warning about an unterminated html tag
+ *
+ * @param el the current element
+ * @param tag the current tag
+ */
+ public void logUnterminatedHtml(Element el, StartElementTree tag) {
+ try {
+ logWarning(el, tag, "unterminated html tag");
+ }
+ catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ /**
+ * Converts the provided HTML to reStructuredText where possible
+ *
+ * @param tag the html
+ * @param el the element containing the html
+ * @param it the Javadoc tree iterator
+ * @return the converted string
+ */
+ String convertHtml(HtmlDocTree tag, Element el, ListIterator extends DocTree> it) {
+ StartElementTree start = tag.getStartTag();
+ return switch (tag.getHtmlKind()) {
+ case A -> convertAnchor(tag, el);
+ case B -> "**" + convertTree(el, tag.getBody()) + "**";
+ case BIG -> ""; // not in rst
+ case BLOCKQUOTE -> convertBlockQuote(tag, el);
+ case BR -> "\n";
+ case CAPTION -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case CITE -> "*" + convertTree(el, tag.getBody()) + "*";
+ case CODE -> "``" + convertTree(el, tag.getBody()) + "``";
+ case DD -> {
+ logError(el, start, " outside of list");
+ yield start.toString();
+ }
+ case DEL -> "~~" + convertTree(el, tag.getBody()) + "~~";
+ // rarely used, not bothering with id attribute
+ case DFN -> "*" + convertTree(el, tag.getBody()) + "*";
+ case DIV -> convertTree(el, tag.getBody()); // do nothing
+ case DL -> convertDescriptionList(tag, el);
+ case DT -> {
+ logError(el, start, " outside of list");
+ yield start.toString();
+ }
+ case EM -> "*" + convertTree(el, tag.getBody()) + "*";
+ case H1 -> convertHeader(tag, el, '#');
+ case H2 -> convertHeader(tag, el, '*');
+ case H3 -> convertHeader(tag, el, '=');
+ case H4 -> convertHeader(tag, el, '-');
+ case H5 -> convertHeader(tag, el, '^');
+ case H6 -> convertHeader(tag, el, '\'');
+ case HR -> "---\n";
+ case I -> "*" + convertTree(el, tag.getBody()) + "*";
+ case IMG -> ""; // not supported because the images wouldn't be available
+ case INS -> convertTree(el, tag.getBody()); // no underline in rst
+ case LI -> {
+ logError(el, start, " outside of list");
+ yield start.toString();
+ }
+ case OL -> convertOrderedList(tag, el);
+ case P -> "\n";
+ case PRE -> convertTree(el, tag.getBody()); // do nothing
+ case SMALL -> ""; // not in rst
+ case SPAN -> convertTree(el, tag.getBody()); // no colored text in rst
+ case STRONG -> "**" + convertTree(el, tag.getBody()) + "**";
+ case SUB -> ""; // no subscript in rst
+ case SUP -> ""; // no superscript in rst
+ case TABLE -> convertTable(tag, el);
+ case TBODY -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case TD -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case TFOOT -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case TH -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case THEAD -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case TR -> {
+ logError(el, start, " outside of table");
+ yield start.toString();
+ }
+ case TT -> "``" + convertTree(el, tag.getBody()) + "``";
+ case U -> convertTree(el, tag.getBody()); // no underline in rst
+ case UL -> convertUnorderedList(tag, el);
+ case UNSUPPORTED -> {
+ logWarning(el, start, "unsupported html tag");
+ yield start.toString();
+ }
+ case VAR -> "*" + convertTree(el, tag.getBody()) + "*";
+ };
+ }
+
+ String convertHtml(StartElementTree start, Element el, ListIterator extends DocTree> it) {
+ HtmlDocTree tag = HtmlDocTree.getTree(this, start, el, it);
+ return convertHtml(tag, el, it);
+ }
+
+ /**
+ * Converts a {@literal } tag
+ *
+ * @param html the html
+ * @param el the element
+ * @return the converted blockquote
+ */
+ private String convertBlockQuote(HtmlDocTree html, Element el) {
+ String body = convertTree(el, html.getBody());
+ return body.indent(INDENT_WIDTH);
+ }
+
+ /**
+ * Converts the {@literal } ... {@literal } tags
+ *
+ * @param html the html
+ * @param el the element
+ * @param header the header character
+ * @return the converted header
+ */
+ private String convertHeader(HtmlDocTree html, Element el, char header) {
+ String body = convertTree(el, html.getBody());
+ int length = body.length();
+ StringBuilder builder = new StringBuilder();
+ return builder.append('\n')
+ .repeat(header, length)
+ .append('\n')
+ .append(body)
+ .append('\n')
+ .repeat(header, length)
+ .append('\n')
+ .toString();
+ }
+
+ /**
+ * Converts a {@literal } tag
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted list entry
+ */
+ private String convertListEntry(HtmlDocTree tree, Element el) {
+ StringBuilder builder = new StringBuilder();
+ for (DocTree tag : tree.getBody()) {
+ if (tag instanceof HtmlDocTree html) {
+ switch (html.getHtmlKind()) {
+ case OL: {
+ String list = convertOrderedList(html, el);
+ builder.append(list.indent(INDENT_WIDTH));
+ break;
+ }
+ case UL: {
+ String list = convertUnorderedList(html, el);
+ builder.append(list.indent(INDENT_WIDTH));
+ break;
+ }
+ default: {
+ builder.append(convertTree(el, html.getBody()));
+ break;
+ }
+ }
+ }
+ else {
+ String entry = docConverter.convertTag(el, tag, null);
+ builder.append(alignIndent(entry));
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts a description list {@literal }
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted list
+ */
+ private String convertDescriptionList(HtmlDocTree tree, Element el) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('\n');
+ for (DocTree tag : tree.getBody()) {
+ if (tag instanceof HtmlDocTree html) {
+ if (html.getHtmlKind() == HtmlTagKind.DT) {
+ builder.append(convertTree(el, html.getBody()));
+ }
+ else if (html.getHtmlKind() == HtmlTagKind.DD) {
+ String body = convertTree(el, html.getBody());
+ builder.append(body.indent(INDENT_WIDTH))
+ .append('\n');
+ }
+ else {
+ builder.append(convertTree(el, html.getBody()));
+ }
+ }
+ else {
+ builder.append(docConverter.convertTag(el, tag, null));
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts an ordered list {@literal }
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted list
+ */
+ private String convertOrderedList(HtmlDocTree tree, Element el) {
+ StringBuilder builder = new StringBuilder();
+ int num = 1; // because #. doesn't always work like it should
+ builder.append('\n');
+ for (DocTree tag : tree.getBody()) {
+ if (tag instanceof HtmlDocTree html) {
+ if (html.getHtmlKind() == HtmlTagKind.LI) {
+ builder.append(num++)
+ .append(". ")
+ .append(convertListEntry(html, el))
+ .append('\n');
+ }
+ else {
+ builder.append(convertTree(el, html.getBody()));
+ }
+ }
+ else {
+ builder.append(docConverter.convertTag(el, tag, null));
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts an unordered list {@literal }
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted list
+ */
+ private String convertUnorderedList(HtmlDocTree tree, Element el) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('\n');
+ for (DocTree tag : tree.getBody()) {
+ if (tag instanceof HtmlDocTree html) {
+ if (html.getHtmlKind() == HtmlTagKind.LI) {
+ builder.append("* ")
+ .append(convertListEntry(html, el))
+ .append('\n');
+ }
+ else {
+ builder.append(convertTree(el, html.getBody()));
+ }
+ }
+ else {
+ builder.append(docConverter.convertTag(el, tag, null));
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts an anchor {@literal link text }
+ *
+ * @param html the html
+ * @param el the element
+ * @return the converted html
+ */
+ private String convertAnchor(HtmlDocTree html, Element el) {
+ String label = convertTree(el, html.getBody()).stripLeading();
+ Map attrs = getAttributes(el, html.getStartTag());
+ String id = attrs.get("id");
+ if (id == null) {
+ id = attrs.get("name");
+ }
+ if (id != null) {
+ return "\n.. _" + id + ":\n\n" + label;
+ }
+
+ String href = attrs.get("href");
+ if (href == null) {
+ logWarning(el, html.getStartTag(), "skipping anchor without an id or href");
+ return "";
+ }
+ if (href.startsWith("#")) {
+ // internal
+ if (label.isBlank()) {
+ return href.substring(1) + '_';
+ }
+ return '`' + label + " <" + href.substring(1) + "_>`_";
+ }
+
+ // external
+ if (label.isBlank()) {
+ return '<' + href.substring(0) + '>';
+ }
+ return '`' + label + " <" + href + ">`_";
+ }
+
+ /**
+ * Converts the provided tree to a raw html string
+ *
+ * @param el the element
+ * @param tree the tree
+ * @return the html string
+ */
+ private String getRawHtml(Element el, List extends DocTree> tree) {
+ StringBuilder builder = new StringBuilder();
+ for (DocTree tag : tree) {
+ switch (tag.getKind()) {
+ case START_ELEMENT:
+ case END_ELEMENT:
+ builder.append(tag.toString());
+ break;
+ case OTHER:
+ if (!(tag instanceof HtmlDocTree)) {
+ logError(el, tag, "Unexpected OTHER tag kind");
+ return "";
+ }
+ HtmlDocTree html = (HtmlDocTree) tag;
+ builder.append(html.getStartTag().toString())
+ .append(getRawHtml(el, html.getBody()));
+ EndElementTree end = html.getEndTag();
+ if (end != null) {
+ builder.append(end.toString());
+ }
+ break;
+ case LINK:
+ case LINK_PLAIN:
+ builder.append(getRawHtml(el, ((LinkTree) tag).getLabel()));
+ break;
+ default:
+ builder.append(docConverter.convertTag(el, tag, null));
+ break;
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts the html tree to a raw html string
+ *
+ * @param html the html tree
+ * @param el the element
+ * @return the html
+ */
+ private String getRawHtml(HtmlDocTree html, Element el) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(html.getStartTag().toString())
+ .append(getRawHtml(el, html.getBody()));
+ EndElementTree end = html.getEndTag();
+ if (end != null) {
+ builder.append(end.toString());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Converts a table {@literal } to reStructuredText if possible
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted table or original html if not convertible
+ */
+ private String convertTable(HtmlDocTree tree, Element el) {
+ try {
+ return tryConvertTable(tree, el);
+ }
+ catch (UnsupportedOperationException e) {
+ // use raw html directive
+ // this may not be supported by all IDEs but it is better then nothing
+ // if your IDE doesn't support it, try tilting your head and squinting
+ StringBuilder builder = new StringBuilder();
+ return builder.append("\n\n.. raw:: html\n\n")
+ .append(getRawHtml(tree, el).indent(INDENT_WIDTH))
+ .append('\n')
+ .toString();
+ }
+ }
+
+ /**
+ * Converts a table {@literal }
+ *
+ * @param tree the html
+ * @param el the element
+ * @return the converted table
+ * @throws UnsupportedOperationException if the table contains nested rows
+ */
+ private String tryConvertTable(HtmlDocTree tree, Element el) {
+ RstTableBuilder tbl = new RstTableBuilder(this, el);
+ ListIterator extends DocTree> it = tree.getBody().listIterator();
+ while (it.hasNext()) {
+ DocTree tag = it.next();
+ switch (tag.getKind()) {
+ case OTHER:
+ if (!(tag instanceof HtmlDocTree)) {
+ logError(el, tag, "Unexpected OTHER tag kind");
+ return "";
+ }
+ HtmlDocTree html = (HtmlDocTree) tag;
+ switch (html.getHtmlKind()) {
+ case TBODY:
+ case TFOOT:
+ case THEAD:
+ tbl.addRowGroup(html);
+ break;
+ case TR:
+ tbl.addRow(html);
+ break;
+ case CAPTION:
+ tbl.addCaption(convertTree(el, html.getBody()));
+ break;
+ default:
+ logError(el, tag,
+ "unexpected html tag encountered while parsing table");
+ break;
+ }
+ break;
+ case TEXT:
+ String body = ((TextTree) tag).getBody();
+ if (!body.isBlank()) {
+ logWarning(el, tag, "skipping unexpected text in table");
+ }
+ break;
+ default:
+ logError(el, tag, "unexpected tag encountered while parsing table");
+ return "";
+ }
+ }
+ return tbl.build();
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlDocTree.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlDocTree.java
new file mode 100644
index 0000000000..05bcce7c94
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlDocTree.java
@@ -0,0 +1,150 @@
+package ghidra.doclets.typestubs;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.lang.model.element.Element;
+
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.DocTreeVisitor;
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.TextTree;
+
+/**
+ * A {@link DocTree} for handling HTML
+ *
+ * This class allows for converting the HTML tags recursively in the same fashion
+ * as the Javadoc tags.
+ */
+public final class HtmlDocTree implements DocTree {
+
+ private final HtmlTagKind kind;
+ private final StartElementTree start;
+ private final EndElementTree end;
+ private final List extends DocTree> body;
+
+ /**
+ * Gets an {@link HtmlDocTree} for the provided {@link StartElementTree}
+ *
+ * @param converter the html converter
+ * @param start the html start
+ * @param el the element containing the documentation being processed
+ * @param it the iterator over the remaining tags
+ * @return the created {@link HtmlDocTree}
+ */
+ public static HtmlDocTree getTree(HtmlConverter converter, StartElementTree start, Element el,
+ ListIterator extends DocTree> it) {
+ HtmlTagKind kind = HtmlTagKind.getKind(start);
+ List body = new ArrayList<>();
+ if (start.isSelfClosing() || HtmlTagKind.isVoidTag(kind)) {
+ return new HtmlDocTree(kind, start, null, body);
+ }
+ while (it.hasNext()) {
+ DocTree tag = it.next();
+ switch (tag.getKind()) {
+ case START_ELEMENT:
+ if (kind.isTerminateBy((StartElementTree) tag)) {
+ // hack for unclosed elements
+ it.previous();
+ converter.logUnterminatedHtml(el, start);
+ return new HtmlDocTree(kind, start, null, body);
+ }
+ body.add(HtmlDocTree.getTree(converter, (StartElementTree) tag, el, it));
+ break;
+ case END_ELEMENT:
+ if (kind.isTerminateBy((EndElementTree) tag)) {
+ // hack for unclosed elements
+ it.previous();
+ converter.logUnterminatedHtml(el, start);
+ return new HtmlDocTree(kind, start, null, body);
+ }
+ if (kind == HtmlTagKind.getKind((EndElementTree) tag)) {
+ return new HtmlDocTree(kind, start, (EndElementTree) tag, body);
+ }
+ body.add(tag);
+ break;
+ case TEXT:
+ String text = ((TextTree) tag).getBody();
+ if (kind != HtmlTagKind.PRE && text.isBlank()) {
+ continue;
+ }
+ body.add(tag);
+ break;
+ default:
+ body.add(tag);
+ break;
+ }
+ }
+ converter.logUnterminatedHtml(el, start);
+ return new HtmlDocTree(kind, start, null, body);
+ }
+
+ /**
+ * Creates a new {@link HtmlDocTree} with the provided fields
+ *
+ * @param kind the html tag kind
+ * @param start the start element
+ * @param end the optional end element
+ * @param body the html body
+ */
+ private HtmlDocTree(HtmlTagKind kind, StartElementTree start, EndElementTree end,
+ List body) {
+ this.kind = kind;
+ this.start = start;
+ this.end = end;
+ this.body = Collections.unmodifiableList(body);
+ }
+
+ @Override
+ public Kind getKind() {
+ // OTHER is implementation reserved
+ // Since this is implementation specific, lets use it
+ return Kind.OTHER;
+ }
+
+ @Override
+ public R accept(DocTreeVisitor visitor, D data) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the html body
+ *
+ * @return the html body
+ */
+ public List extends DocTree> getBody() {
+ return body;
+ }
+
+ /**
+ * Gets the html tag kind
+ *
+ * @return the html tag kind
+ */
+ public HtmlTagKind getHtmlKind() {
+ return kind;
+ }
+
+ /**
+ * Gets the html start element tree
+ *
+ * @return the html start element
+ */
+ public StartElementTree getStartTag() {
+ return start;
+ }
+
+ /**
+ * Gets the html end element tree
+ *
+ * This may be null if the html tag is a "void" tag or if the html is malformed
+ *
+ * @return the html end element or null
+ */
+ public EndElementTree getEndTag() {
+ return end;
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlTagKind.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlTagKind.java
new file mode 100644
index 0000000000..f2122c258a
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/HtmlTagKind.java
@@ -0,0 +1,351 @@
+package ghidra.doclets.typestubs;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.StartElementTree;
+
+public enum HtmlTagKind {
+ // This would be much simpler if we didn't have to handle malformed html
+ // HTML container tags REQUIRE a closing tag
+ // Unfortunately they are often ommitted, even in the JDK API, which makes
+ // this much more complicated then it needs to be.
+ // Best we can do it try not to consume elements that can't possibly be ours,
+ // log it when encountered and then hope the result isn't ruined.
+
+ A,
+ B,
+ BIG,
+ BLOCKQUOTE {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return kind == this;
+ }
+ },
+ BR,
+ CAPTION,
+ CITE,
+ CODE,
+ DD {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case DD, DT, DL -> true;
+ default -> false;
+ };
+ }
+ },
+ DEL,
+ DFN,
+ DIV,
+ DL {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case DL -> true;
+ default -> false;
+ };
+ }
+ },
+ DT {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case DD, DT, DL -> true;
+ default -> false;
+ };
+ }
+ },
+ EM,
+ H1 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ H2 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ H3 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ H4 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ H5 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ H6 {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ if (isInline(kind)) {
+ return false;
+ }
+ return switch (kind) {
+ case A -> false;
+ default -> true;
+ };
+ }
+ },
+ HR,
+ I,
+ IMG,
+ INS,
+ LI {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case LI -> true;
+ default -> false;
+ };
+ }
+
+ @Override
+ public boolean isTerminateBy(EndElementTree end) {
+ return switch (getKind(end)) {
+ case OL, UL -> true;
+ default -> false;
+ };
+ }
+ },
+ OL {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return false;
+ }
+ },
+ P,
+ PRE {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return false;
+ }
+ },
+ SMALL,
+ SPAN,
+ STRONG,
+ SUB,
+ SUP,
+ TABLE {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ // no nested tables
+ return kind == this;
+ }
+ },
+ TBODY {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case THEAD, TFOOT, TBODY, TABLE -> true;
+ default -> false;
+ };
+ }
+ },
+ TD {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case TD, TH, TR, THEAD, TFOOT, TBODY, TABLE -> true;
+ default -> false;
+ };
+ }
+ },
+ TFOOT {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case THEAD, TFOOT, TBODY, TABLE -> true;
+ default -> false;
+ };
+ }
+ },
+ TH {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case TD, TH, TR, THEAD, TFOOT, TBODY, TABLE -> true;
+ default -> false;
+ };
+ }
+ },
+ THEAD {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case THEAD, TFOOT, TBODY, TABLE -> true;
+ default -> false;
+ };
+ }
+ },
+ TR {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return switch (kind) {
+ case TR, TABLE, THEAD, TFOOT, TBODY -> true;
+ default -> false;
+ };
+ }
+ },
+ TT,
+ U,
+ UL {
+ @Override
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return false;
+ }
+ },
+ VAR,
+ UNSUPPORTED;
+
+ private static final Map LOOKUP;
+
+ static {
+ HtmlTagKind[] values = values();
+ LOOKUP = new HashMap<>(values.length);
+ for (HtmlTagKind value : values) {
+ LOOKUP.put(value.name(), value);
+ }
+ }
+
+ /**
+ * Gets the HtmlTagKind with the provided name
+ *
+ * @param name the name
+ * @return the HtmlTagKind with the same name or UNSUPPORTED
+ */
+ static HtmlTagKind getKind(String name) {
+ return LOOKUP.getOrDefault(name, UNSUPPORTED);
+ }
+
+ /**
+ * Gets the HtmlTagKind for the provided element
+ *
+ * @param tag the tag
+ * @return the HtmlTagKind for the provided tag or UNSUPPORTED
+ */
+ static HtmlTagKind getKind(StartElementTree tag) {
+ return getKind(tag.getName().toString().toUpperCase());
+ }
+
+ /**
+ * Gets the HtmlTagKind for the provided element
+ *
+ * @param tag the tag
+ * @return the HtmlTagKind for the provided tag or UNSUPPORTED
+ */
+ static HtmlTagKind getKind(EndElementTree tag) {
+ return getKind(tag.getName().toString().toUpperCase());
+ }
+
+ /**
+ * Checks if this tag is terminated by another tag because it can't possibly contain it
+ *
+ * @param kind the other HtmlTagKind
+ * @return true if this tag canot possibly contain the other kind
+ */
+ boolean isTerminateBy(HtmlTagKind kind) {
+ return !isInline(kind);
+ }
+
+ /**
+ * Checks if this tag is terminated by another element because it can't possibly contain it
+ *
+ * @param kind the other HtmlTagKind
+ * @return true if this tag canot possibly contain the other element
+ */
+ public final boolean isTerminateBy(StartElementTree start) {
+ HtmlTagKind kind = getKind(start);
+ return isTerminateBy(kind);
+ }
+
+ /**
+ * Checks if this tag is terminated by the closing another element.
+ *
+ * This is usually because the other element would contain it.
+ *
+ * @param kind the other HtmlTagKind
+ * @return true if this tag canot possibly contain the other kind
+ */
+ public boolean isTerminateBy(EndElementTree end) {
+ HtmlTagKind kind = getKind(end);
+ if (kind == this) {
+ // this tag may not be for the current node so we return false here
+ return false;
+ }
+ return isTerminateBy(kind);
+ }
+
+ /**
+ * Checks if the provided tag is a void or empty tag
+ *
+ * @param kind the tag kind
+ * @return true if this is a void or empty tag
+ */
+ public static boolean isVoidTag(HtmlTagKind kind) {
+ // technically is NOT a void tag
+ // unfortunately it is misused so often that the errors/warnings
+ // would become junk because the
tags would have consumed too much
+ return switch (kind) {
+ case BR, HR, P -> true;
+ default -> false;
+ };
+ }
+
+ /**
+ * Checks if the provided tag is for inline markup
+ *
+ * @param kind the tag kind
+ * @return true if this kind is for inline markup
+ */
+ public static boolean isInline(HtmlTagKind kind) {
+ return switch (kind) {
+ case B, BIG, CITE, DFN, CODE, DEL, EM, I, INS -> true;
+ case SMALL, STRONG, SUB, SUP, TT, U, VAR -> true;
+ default -> false;
+ };
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/JavadocConverter.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/JavadocConverter.java
new file mode 100644
index 0000000000..ceeec4a919
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/JavadocConverter.java
@@ -0,0 +1,681 @@
+package ghidra.doclets.typestubs;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+import com.sun.source.doctree.*;
+
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+/**
+ * Helper class for converting Javadoc to Python docstring format
+ */
+public class JavadocConverter extends DocConverter {
+
+ private static final Pattern LEADING_WHITESPACE = Pattern.compile("(\\s+)\\S.*");
+
+ private static final Map AUTO_CONVERSIONS = new HashMap<>(
+ Map.ofEntries(
+ Map.entry("java.lang.Boolean", "java.lang.Boolean or bool"),
+ Map.entry("java.lang.Byte", "java.lang.Byte or int"),
+ Map.entry("java.lang.Character", "java.lang.Character or int or str"),
+ Map.entry("java.lang.Double", "java.lang.Double or float"),
+ Map.entry("java.lang.Float", "java.lang.Float or float"),
+ Map.entry("java.lang.Integer", "java.lang.Integer or int"),
+ Map.entry("java.lang.Long", "java.lang.Long or int"),
+ Map.entry("java.lang.Short", "java.lang.Short or int"),
+ Map.entry("java.lang.String", "java.lang.String or str"),
+ Map.entry("java.io.File", "jpype.protocol.SupportsPath"),
+ Map.entry("java.nio.file.Path", "jpype.protocol.SupportsPath"),
+ Map.entry("java.lang.Iterable", "collections.abc.Sequence"),
+ Map.entry("java.util.Collection", "collections.abc.Sequence"),
+ Map.entry("java.util.Map", "collections.abc.Mapping"),
+ Map.entry("java.time.Instant", "datetime.datetime"),
+ Map.entry("java.sql.Time", "datetime.time"),
+ Map.entry("java.sql.Date", "datetime.date"),
+ Map.entry("java.sql.Timestamp", "datetime.datetime"),
+ Map.entry("java.math.BigDecimal", "decimal.Decimal")));
+
+ // these tags are used in the jdk and shouldn't cause any warnings
+ // it is not worth the effort to handle them to output any documentation
+ private static final Set JDK_TAGLETS = new HashSet<>(
+ Set.of("jls", "jvms", "extLink", "Incubating", "moduleGraph", "sealedGraph", "toolGuide"));
+
+ private static final Map NOTE_TAGLETS = new HashMap<>(
+ Map.of("apiNote", "API Note", "implNote", "Implementation Note", "implSpec",
+ "Implementation Requirements"));
+
+ private final HtmlConverter htmlConverter;
+
+ /**
+ * Creates a new {@link DocConverter}
+ *
+ * @param env the doclet environment
+ * @param log the log
+ */
+ public JavadocConverter(DocletEnvironment env, Reporter log) {
+ super(env, log);
+ this.htmlConverter = new HtmlConverter(env, log, this);
+ }
+
+ /**
+ * Gets the Javadoc for the provided element
+ *
+ * @param el the element
+ * @return the Javadoc
+ */
+ String getJavadoc(Element el) {
+ return getJavadoc(el, getDocTrees().getDocCommentTree(el));
+ }
+
+ /**
+ * Gets the Javadoc tree for the provided element
+ *
+ * @param el the element
+ * @return the Javadoc tree
+ */
+ DocCommentTree getJavadocTree(Element el) {
+ return getDocTrees().getDocCommentTree(el);
+ }
+
+ /**
+ * Gets the converted documentation for the provided element and doc tree
+ *
+ * @param el the element
+ * @param docCommentTree the doc tree
+ * @return the converted documentation
+ */
+ private String getJavadoc(Element el, DocCommentTree docCommentTree) {
+ if (docCommentTree != null) {
+ StringBuilder builder = new StringBuilder();
+ ListIterator extends DocTree> it = docCommentTree.getFullBody().listIterator();
+ while (it.hasNext()) {
+ DocTree next = it.next();
+ builder.append(convertTag(el, next, it));
+ }
+ // A blank line is required before block tags
+ builder.append("\n\n");
+ List seealso = new ArrayList<>();
+ it = docCommentTree.getBlockTags().listIterator();
+ while (it.hasNext()) {
+ DocTree tag = it.next();
+ if (tag.getKind() == DocTree.Kind.SEE) {
+ seealso.add((SeeTree) tag);
+ continue;
+ }
+ if (tag.getKind() == DocTree.Kind.HIDDEN) {
+ // hidden blocktag means don't document
+ return "";
+ }
+ builder.append(convertTag(el, tag, it));
+ }
+ if (!seealso.isEmpty()) {
+ builder.append("\n.. seealso::\n\n");
+ for (SeeTree tag : seealso) {
+ String message = "| " + alignIndent(convertTree(el, tag.getReference()));
+ builder.append(message.indent(INDENT_WIDTH))
+ .append('\n');
+ }
+
+ }
+ String tmp = builder.toString().replaceAll("\t", " ");
+ if (tmp.indexOf('\n') == -1) {
+ return tmp;
+ }
+ builder = new StringBuilder(tmp.length());
+
+ // we need to fix the indentation because it will mess with the reStructured text
+ // NOTE: you cannot just use String.stripLeading or String.indent(-1) here
+ Iterable lines = () -> tmp.lines().iterator();
+ for (String line : lines) {
+ Matcher matcher = LEADING_WHITESPACE.matcher(line);
+ if (matcher.matches()) {
+ String whitespace = matcher.group(1);
+ builder.append(line.substring(whitespace.length() % INDENT_WIDTH))
+ .append('\n');
+ }
+ else {
+ builder.append(line)
+ .append('\n');
+ }
+ }
+ return builder.toString();
+ }
+ return "";
+ }
+
+ @Override
+ String convertTag(Element el, DocTree tag, ListIterator extends DocTree> it) {
+ // NOTE: each tag is responsible for its own line endings
+ return switch (tag.getKind()) {
+ case DOC_ROOT -> tag.toString(); // not sure what would be an appropriate replacement
+ case PARAM -> convertParamTag(el, (ParamTree) tag);
+ case RETURN -> convertReturnTag((ExecutableElement) el, (ReturnTree) tag);
+ case THROWS -> convertThrowsTag((ExecutableElement) el, (ThrowsTree) tag);
+ case START_ELEMENT -> convertHTML(el, (StartElementTree) tag, it);
+ case END_ELEMENT -> convertHTML((EndElementTree) tag);
+ case LINK -> convertLinkTag(el, (LinkTree) tag);
+ case LINK_PLAIN -> convertLinkTag(el, (LinkTree) tag);
+ case EXCEPTION -> convertThrowsTag((ExecutableElement) el, (ThrowsTree) tag);
+ case ENTITY -> convertEntity((EntityTree) tag);
+ case CODE -> convertCodeTag((LiteralTree) tag);
+ case LITERAL -> convertLiteralTag((LiteralTree) tag);
+ case VALUE -> convertValueTag(el, (ValueTree) tag);
+ case DEPRECATED -> convertDeprecatedTag(el, (DeprecatedTree) tag);
+ case REFERENCE -> convertReferenceTag(el, (ReferenceTree) tag);
+ case SINCE -> convertSinceTag(el, (SinceTree) tag);
+ case AUTHOR -> convertAuthorTag(el, (AuthorTree) tag);
+ case VERSION -> ""; // ignored
+ case ERRONEOUS -> {
+ logError(el, tag, "erroneous javadoc tag");
+ yield tag.toString();
+ }
+ case UNKNOWN_BLOCK_TAG -> convertUnknownBlockTag(el, (UnknownBlockTagTree) tag);
+ case UNKNOWN_INLINE_TAG -> {
+ if (JDK_TAGLETS.contains(((UnknownInlineTagTree) tag).getTagName())) {
+ yield "";
+ }
+ logError(el, tag, "unknown javadoc inline tag");
+ yield tag.toString();
+ }
+ case TEXT -> ((TextTree) tag).getBody();
+ case SNIPPET -> convertSnippet(el, (SnippetTree) tag);
+ case INHERIT_DOC -> ""; // ignored, anything containing this is skipped
+ case OTHER -> {
+ if (tag instanceof HtmlDocTree html) {
+ yield htmlConverter.convertHtml(html, el, it);
+ }
+ else {
+ yield tag.toString();
+ }
+ }
+ case SPEC -> "";
+ case SERIAL -> "";
+ case SERIAL_DATA -> "";
+ case SYSTEM_PROPERTY -> "``" + ((SystemPropertyTree) tag).getPropertyName() + "``";
+ case COMMENT -> "";
+ case INDEX -> "";
+ default -> {
+ logWarning(el, tag, "unsupported javadoc tag");
+ yield tag.toString();
+ }
+ case ESCAPE -> ((EscapeTree) tag).getBody();
+ case SERIAL_FIELD -> "";
+ case SUMMARY -> convertTree(el, ((SummaryTree) tag).getSummary());
+ case USES -> "";
+ };
+ }
+
+ private String convertUnknownBlockTag(Element el, UnknownBlockTagTree tag) {
+ if (JDK_TAGLETS.contains(tag.getTagName())) {
+ return "";
+ }
+ String title = NOTE_TAGLETS.get(tag.getTagName());
+ if (title == null) {
+ logError(el, tag, "unknown javadoc block tag");
+ return tag.toString();
+ }
+ StringBuilder builder = new StringBuilder();
+ String message = alignIndent(convertTree(el, tag.getContent()));
+ return builder.append("\n.. admonition:: ")
+ .append(title)
+ .append("\n\n")
+ .append(message.indent(INDENT_WIDTH))
+ .append("\n\n")
+ .toString();
+ }
+
+ /**
+ * Gets the attributes for the provided snippet
+ *
+ * @param snippet the snippet
+ * @return the snippet attributes
+ */
+ private Map getAttributes(Element el, SnippetTree snippet) {
+ return getAttributes(el, snippet.getAttributes());
+ }
+
+ /**
+ * Indent the provided text
+ *
+ * @param text the text to indent
+ * @return the indented text
+ */
+ private static String indent(String text) {
+ return text.indent(INDENT_WIDTH);
+ }
+
+ /**
+ * Indent the provided text tree
+ *
+ * @param text the text tree
+ * @return the indented text
+ */
+ private static String indent(TextTree text) {
+ return indent(text.getBody());
+ }
+
+ /**
+ * Converts an author Javadoc tag
+ *
+ * @param el the current element
+ * @param author the author tag
+ * @return the converted tag
+ */
+ private String convertAuthorTag(Element el, AuthorTree author) {
+ String name = convertTree(el, author.getName());
+ return "\n.. codeauthor:: " + name + '\n';
+ }
+
+ /**
+ * Converts a since Javadoc tag
+ *
+ * @param el the current element
+ * @param since the since tag
+ * @return the converted tag
+ */
+ private String convertSinceTag(Element el, SinceTree since) {
+ // NOTE: there must be a preceeding new line
+ String msg = convertTree(el, since.getBody());
+ return "\n.. versionadded:: " + msg + '\n';
+ }
+
+ /**
+ * Converts a link Javadoc tag
+ *
+ * @param el the current element
+ * @param link the link tag
+ * @return the converted tag
+ */
+ private String convertLinkTag(Element el, LinkTree link) {
+ String sig = link.getReference().getSignature().replaceAll("#", ".");
+ int index = sig.indexOf('(');
+ String label = convertTree(el, link.getLabel());
+ if (index != -1) {
+ String name = sig;
+ sig = sig.substring(0, index);
+ if (label.isBlank()) {
+ if (name.startsWith(".")) {
+ label = name.substring(1);
+ }
+ else {
+ label = name;
+ }
+ }
+ return ":meth:`" + label + " <" + sig + ">`";
+ }
+ if (!label.isBlank()) {
+ return ":obj:`" + label + " <" + sig + ">`";
+ }
+ return ":obj:`" + sig + '`';
+ }
+
+ /**
+ * Gets the constant value for a value tag
+ *
+ * @param el the current element
+ * @param tag the value tag
+ * @return the constant value
+ */
+ private static String getConstantValue(VariableElement el, ValueTree tag) {
+ Object value = el.getConstantValue();
+ TextTree format = tag.getFormat();
+ if (format != null) {
+ try {
+ return String.format(format.getBody(), value);
+ }
+ catch (IllegalArgumentException e) {
+ // fallthrough
+ }
+ }
+ return value.toString();
+ }
+
+ /**
+ * Converts a Javadoc reference
+ *
+ * @param el the current element
+ * @param ref the reference
+ * @return the converted reference
+ */
+ private String convertReferenceTag(Element el, ReferenceTree ref) {
+ String sig = ref.getSignature();
+ if (sig == null || sig.isBlank()) {
+ return "";
+ }
+ return ":obj:`" + sig.replace('#', '.') + '`';
+ }
+
+ /**
+ * Converts a value Javadoc tag
+ *
+ * @param el the current element
+ * @param value the value tag
+ * @return the converted tag
+ */
+ private String convertValueTag(Element el, ValueTree value) {
+ ReferenceTree ref = value.getReference();
+ if (ref == null) {
+ return "";
+ }
+ String sig = ref.getSignature();
+ if (sig == null || sig.isBlank()) {
+ if (el instanceof VariableElement var) {
+ return getConstantValue(var, value);
+ }
+ return ":const:`" + sig.replaceAll("#", ".") + '`';
+ }
+ int index = sig.indexOf('#');
+ TypeElement type;
+ String field;
+ if (index == 0) {
+ if (el instanceof ExecutableElement method) {
+ type = (TypeElement) method.getEnclosingElement();
+ }
+ else {
+ type = (TypeElement) el;
+ }
+ field = sig.substring(1);
+ }
+ else {
+ String name = sig.substring(0, index);
+ type = getElementUtils().getTypeElement(name);
+ if (type == null && el instanceof ExecutableElement method) {
+ // check if the name of the current class was specified
+ type = (TypeElement) method.getEnclosingElement();
+ if (!type.getSimpleName().contentEquals(name)) {
+ type = null;
+ }
+ }
+ field = sig.substring(index + 1);
+ }
+ if (type != null) {
+ for (Element child : getElementUtils().getAllMembers(type)) {
+ if (child.getSimpleName().contentEquals(field)) {
+ if (child instanceof VariableElement var) {
+ return getConstantValue(var, value);
+ }
+ }
+ }
+ }
+ return ":const:`" + sig.replaceAll("#", ".") + '`';
+ }
+
+ /**
+ * Converts a deprecated Javadoc tag
+ *
+ * @param tag the deprecated tag
+ * @return the converted tag
+ */
+ private String convertDeprecatedTag(Element el, DeprecatedTree tag) {
+ String body = convertTree(el, tag.getBody());
+ return new StringBuilder("\n.. deprecated::\n\n")
+ .append(body)
+ .append('\n')
+ .toString();
+ }
+
+ /**
+ * Converts a snippet Javadoc tag
+ *
+ * @param snippet the snippet tag
+ * @return the converted tag
+ */
+ private String convertSnippet(Element el, SnippetTree snippet) {
+ // let pygments guess the code type
+ TextTree body = snippet.getBody();
+ if (body == null) {
+ // there are invalid snippet tags in the internal jdk packages
+ return "";
+ }
+
+ Map attributes = getAttributes(el, snippet);
+ String lang = attributes.getOrDefault("lang", "guess");
+ // any other attributes are not supported
+ return new StringBuilder(".. code-block:: ")
+ .append(lang)
+ .append("\n :dedent: 4\n\n")
+ .append(indent(body))
+ .append('\n')
+ .toString();
+ }
+
+ /**
+ * Converts a code Javadoc tag
+ *
+ * @param code the code tag
+ * @return the converted tag
+ */
+ private static String convertCodeTag(LiteralTree code) {
+ String body = convertLiteralTag(code);
+ if (body.isBlank()) {
+ return "";
+ }
+ return "``" + body + "``";
+ }
+
+ /**
+ * Converts a literal Javadoc tag
+ *
+ * @param literal the literal tag
+ * @return the converted tag
+ */
+ private static String convertLiteralTag(LiteralTree literal) {
+ // NOTE: the literal tag DOES NOT preserve line endings or whitespace
+ // it is still present in the body so remove it
+ TextTree text = literal.getBody();
+ if (text == null) {
+ return "";
+ }
+
+ String body = text.getBody();
+ if (body == null) {
+ return "";
+ }
+
+ return body.stripIndent().replaceAll("\n", "");
+ }
+
+ /**
+ * Converts a html entity (ie. {@literal <})
+ *
+ * @param entity the entity
+ * @return the converted entity
+ */
+ private String convertEntity(EntityTree entity) {
+ return getDocTrees().getCharacters(entity);
+ }
+
+ /**
+ * Converts a html tag
+ *
+ * @param tag the html start tag
+ * @return the converted html
+ */
+ private String convertHTML(Element el, StartElementTree tag,
+ ListIterator extends DocTree> it) {
+ return htmlConverter.convertHtml(tag, el, it);
+ }
+
+ /**
+ * Converts a html tag
+ *
+ * @param tag the html end tag
+ * @return the converted html
+ */
+ private static String convertHTML(EndElementTree tag) {
+ if (tag.getName().contentEquals("p")) {
+ return "\n";
+ }
+ return tag.toString();
+ }
+
+ /**
+ * Sanitizes the provided type with respect to the provided method element
+ *
+ * @param el the method element
+ * @param type the type
+ * @return the sanitized type name
+ */
+ private static String sanitizeQualifiedName(ExecutableElement el, TypeMirror type) {
+ Element self = el.getEnclosingElement();
+ PackageElement pkg = PythonTypeStubElement.getPackage(self);
+ return PythonTypeStubElement.sanitizeQualifiedName(self, type, pkg);
+ }
+
+ /**
+ * Converts a param Javadoc tag for a method parameter
+ *
+ * @param el the current element
+ * @param param the param tag
+ * @return the converted tag
+ */
+ private String convertParamTag(Element el, ParamTree param) {
+ if (el instanceof ExecutableElement executableElement) {
+ return convertParamTag(executableElement, param);
+ }
+ return convertParamTag((TypeElement) el, param);
+ }
+
+ /**
+ * Converts a param Javadoc tag
+ *
+ * @param el the current element
+ * @param param the param tag
+ * @return the converted tag
+ */
+ private static String convertParamTag(TypeElement el, ParamTree param) {
+ // I'm not sure python does this?
+ return "";
+ }
+
+ /**
+ * Converts the parameter type type to show all possible values
+ *
+ * @param type the type to convert
+ * @return the type or null if not applicable
+ */
+ private static String convertParamType(TypeMirror type) {
+ if (type.getKind().isPrimitive()) {
+ return switch (type.getKind()) {
+ case BOOLEAN -> "jpype.JBoolean or bool";
+ case BYTE -> "jpype.JByte or int";
+ case CHAR -> "jpype.JChar or int or str";
+ case DOUBLE -> "jpype.JDouble or float";
+ case FLOAT -> "jpype.JFloat or float";
+ case INT -> "jpype.JInt or int";
+ case LONG -> "jpype.JLong or int";
+ case SHORT -> "jpype.JShort or int";
+ default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
+ };
+ }
+ if (type instanceof DeclaredType dt) {
+ Element element = dt.asElement();
+ if (element instanceof QualifiedNameable nameable) {
+ return AUTO_CONVERSIONS.get(nameable.getQualifiedName().toString());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Converts a param Javadoc tag for a method parameter
+ *
+ * @param el the current element
+ * @param param the param tag
+ * @return the converted tag
+ */
+ private String convertParamTag(ExecutableElement el, ParamTree param) {
+ TypeMirror type = null;
+ for (VariableElement child : el.getParameters()) {
+ if (child.getSimpleName().equals(param.getName().getName())) {
+ type = child.asType();
+ break;
+ }
+ }
+ String description = convertTree(el, param.getDescription());
+ if (type == null) {
+ return ":param " + param.getName() + ": " + description;
+ }
+ String typename = convertParamType(type);
+ if (typename == null) {
+ typename = sanitizeQualifiedName(el, type);
+ }
+ return ":param " + typename + " " + param.getName() + ": " + description + '\n';
+ }
+
+ /**
+ * Converts a return Javadoc tag
+ *
+ * @param el the current element
+ * @param tag the return tag
+ * @return the converted tag
+ */
+ private String convertReturnTag(ExecutableElement el, ReturnTree tag) {
+ String description = convertTree(el, tag.getDescription());
+ if (el.getReturnType().getKind() == TypeKind.VOID) {
+ return ":return: " + description + '\n';
+ }
+
+ String typename = PythonTypeStubMethod.convertResultType(el.getReturnType());
+ if (typename == null) {
+ typename = sanitizeQualifiedName(el, el.getReturnType());
+ }
+ String res = ":return: " + description + '\n';
+ return res + ":rtype: " + typename + '\n';
+ }
+
+ /**
+ * Converts a throws Javadoc tag
+ *
+ * @param el the current element
+ * @param tag the throws tag
+ * @return the converted tag
+ */
+ private String convertThrowsTag(ExecutableElement el, ThrowsTree tag) {
+ String typename = tag.getExceptionName().getSignature();
+ TypeMirror type = null;
+ for (TypeMirror thrownType : el.getThrownTypes()) {
+ if (thrownType.getKind() == TypeKind.TYPEVAR) {
+ if (thrownType.toString().equals(typename)) {
+ break;
+ }
+ continue;
+ }
+ TypeElement typeElement = (TypeElement) (((DeclaredType) thrownType).asElement());
+ if (typeElement.getQualifiedName().contentEquals(typename)) {
+ type = thrownType;
+ break;
+ }
+ if (typeElement.getQualifiedName().toString().startsWith("java.lang.")) {
+ if (typeElement.getSimpleName().contentEquals(typename)) {
+ type = thrownType;
+ break;
+ }
+ }
+ }
+ if (type != null) {
+ typename = sanitizeQualifiedName(el, type);
+ }
+ String description = convertTree(el, tag.getDescription());
+ return ":raises " + typename + ": " + description + '\n';
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubDoclet.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubDoclet.java
new file mode 100644
index 0000000000..66dd8fb2aa
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubDoclet.java
@@ -0,0 +1,526 @@
+/* ###
+ * IP: GHIDRA
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ghidra.doclets.typestubs;
+
+import java.io.*;
+import java.util.*;
+
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.*;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.Diagnostic.Kind;
+
+import com.sun.source.doctree.DeprecatedTree;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.TextTree;
+
+import jdk.javadoc.doclet.*;
+
+/**
+ * Doclet that outputs Python pyi files.
+ *
+ * To run: gradle createPythonTypeStubs
+ */
+public class PythonTypeStubDoclet implements Doclet {
+
+ private Reporter log;
+ private File destDir;
+
+ private DocletEnvironment docEnv;
+ private JavadocConverter docConverter;
+ private Set processedPackages;
+ private Set topLevelPackages;
+ private boolean useAllTypes = false;
+ private boolean useProperties = true;
+ private boolean ghidraMode = false;
+
+ @Override
+ public void init(Locale locale, Reporter reporter) {
+ this.log = reporter;
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.RELEASE_21;
+ }
+
+ @Override
+ public Set extends Option> getSupportedOptions() {
+ return Set.of(new Option() {
+ @Override
+ public int getArgumentCount() {
+ return 1;
+ }
+
+ @Override
+ public String getDescription() {
+ return "the destination directory";
+ }
+
+ @Override
+ public Kind getKind() {
+ return Option.Kind.STANDARD;
+ }
+
+ @Override
+ public List getNames() {
+ return Arrays.asList("-d");
+ }
+
+ @Override
+ public String getParameters() {
+ return "directory";
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ destDir = new File(arguments.get(0)).getAbsoluteFile();
+ return true;
+ }
+
+ },
+ new Option() {
+ @Override
+ public int getArgumentCount() {
+ return 0;
+ }
+
+ @Override
+ public String getDescription() {
+ return "enables Ghidra specific output";
+ }
+
+ @Override
+ public Kind getKind() {
+ return Option.Kind.OTHER;
+ }
+
+ @Override
+ public List getNames() {
+ return Arrays.asList("-ghidra");
+ }
+
+ @Override
+ public String getParameters() {
+ return "";
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ ghidraMode = true;
+ return true;
+ }
+
+ },
+ new Option() {
+ @Override
+ public int getArgumentCount() {
+ return 0;
+ }
+
+ @Override
+ public String getDescription() {
+ return "enables generation of properties from get/set/is methods";
+ }
+
+ @Override
+ public Kind getKind() {
+ return Option.Kind.OTHER;
+ }
+
+ @Override
+ public List getNames() {
+ return Arrays.asList("-properties");
+ }
+
+ @Override
+ public String getParameters() {
+ return "";
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ useProperties = true;
+ return true;
+ }
+
+ });
+ }
+
+ @Override
+ public boolean run(DocletEnvironment env) {
+
+ docEnv = env;
+ docConverter = new JavadocConverter(env, log);
+
+ processedPackages = new HashSet<>();
+ topLevelPackages = new HashSet<>();
+
+ // Create destination directory
+ if (destDir == null) {
+ log.print(Kind.ERROR, "Destination directory not set");
+ return false;
+ }
+ if (!destDir.exists()) {
+ if (!destDir.mkdirs()) {
+ log.print(Kind.ERROR, "Failed to create destination directory at: " + destDir);
+ return false;
+ }
+ }
+
+ Elements elements = docEnv.getElementUtils();
+ Set modules = ElementFilter.modulesIn(docEnv.getSpecifiedElements());
+ if (!modules.isEmpty()) {
+ useAllTypes = true;
+ modules.stream()
+ .map(ModuleElement::getDirectives)
+ .flatMap(List::stream)
+ // only exported packages
+ .filter(d -> d.getKind() == ModuleElement.DirectiveKind.EXPORTS)
+ .map(ModuleElement.ExportsDirective.class::cast)
+ // only exported to ALL-UNNAMED
+ .filter(export -> export.getTargetModules() == null)
+ .map(ModuleElement.ExportsDirective::getPackage)
+ .map((el) -> new PythonTypeStubPackage(this, el))
+ .forEach(PythonTypeStubPackage::process);
+ return true;
+ }
+
+ Set packages = ElementFilter.packagesIn(docEnv.getSpecifiedElements());
+ if (!packages.isEmpty()) {
+ useAllTypes = true;
+ packages.stream()
+ .map((el) -> new PythonTypeStubPackage(this, el))
+ .forEach(PythonTypeStubPackage::process);
+ return true;
+ }
+
+ // it is not safe to use parallelStream :(
+ ElementFilter.typesIn(docEnv.getSpecifiedElements())
+ .stream()
+ .map(elements::getPackageOf)
+ .distinct()
+ .map((el) -> new PythonTypeStubPackage(this, el))
+ .forEach(PythonTypeStubPackage::process);
+
+ // ghidra docs always explicitly specifies the types
+ // so we only need to check the option here
+ if (ghidraMode) {
+ GhidraBuiltinsBuilder builder = new GhidraBuiltinsBuilder(this);
+ builder.process();
+ }
+
+ return true;
+ }
+
+ /**
+ * Prints all the imports in the provided collection
+ *
+ * If a provided import is not included in the output of this doclet, "#type: ignore"
+ * will be appended to the import. This prevents the type checker from treating the
+ * import as an error if the package is not found.
+ *
+ * @param printer the printer
+ * @param packages the packages to import
+ */
+ void printImports(PrintWriter printer, Collection packages) {
+ for (PackageElement pkg : packages) {
+ String name = PythonTypeStubElement.sanitizeQualifiedName(pkg);
+ printer.print("import ");
+ printer.print(name);
+ if (!isIncluded(pkg)) {
+ printer.println(" # type: ignore");
+ }
+ else {
+ printer.println();
+ }
+ }
+ }
+
+ /**
+ * Checks if the provided element is deprecated
+ *
+ * @param el the element to check
+ * @return true if the element is deprecated
+ */
+ boolean isDeprecated(Element el) {
+ return docEnv.getElementUtils().isDeprecated(el);
+ }
+
+ /**
+ * Gets the ElementUtils for the current doclet environment
+ *
+ * @return the ElementUtils
+ */
+ Elements getElementUtils() {
+ return docEnv.getElementUtils();
+ }
+
+ /**
+ * Gets an appropriate message to be used in the warnings.deprecated decorator
+ *
+ * @param el the deprecated element
+ * @return the deprecation message or null if no deprecation reason is documented
+ */
+ String getDeprecatedMessage(Element el) {
+ DocCommentTree tree = docConverter.getJavadocTree(el);
+ if (tree == null) {
+ return null;
+ }
+
+ DeprecatedTree deprecatedTag = tree.getBlockTags()
+ .stream()
+ .filter(tag -> tag.getKind() == DocTree.Kind.DEPRECATED)
+ .map(DeprecatedTree.class::cast)
+ .findFirst()
+ .orElse(null);
+ if (deprecatedTag == null) {
+ return null;
+ }
+
+ String res = getPlainDocString(deprecatedTag.getBody());
+ // NOTE: this must be a safe string literal
+ return getStringLiteral(res);
+ }
+
+ /**
+ * Checks if the provided element is specified to be included by this doclet
+ *
+ * @param element the element to check
+ * @return
+ */
+ boolean isSpecified(Element element) {
+ return useAllTypes || docEnv.getSpecifiedElements().contains(element);
+ }
+
+ /**
+ * Gets the TypeUtils for the current doclet environment
+ *
+ * @return the TypeUtils
+ */
+ Types getTypeUtils() {
+ return docEnv.getTypeUtils();
+ }
+
+ /**
+ * Gets the output directory for the doclet
+ *
+ * @return the output directory
+ */
+ File getDestDir() {
+ return destDir;
+ }
+
+ /**
+ * Gets the documentation for the provided element
+ *
+ * @param el the element
+ * @return the elements documentation
+ */
+ String getJavadoc(Element el) {
+ return docConverter.getJavadoc(el);
+ }
+
+ /**
+ * Checks if this element has any documentation
+ *
+ * @param el the element
+ * @return true if this element has documentation
+ */
+ boolean hasJavadoc(Element el) {
+ DocCommentTree tree = docConverter.getJavadocTree(el);
+ if (tree == null) {
+ return false;
+ }
+ return !tree.getFullBody().toString().isBlank();
+ }
+
+ /**
+ * Checks if this element has the provided Javadoc tag
+ *
+ * @param el the element
+ * @param kind the tag kind
+ * @return true if this element uses the provided Javadoc tag
+ */
+ boolean hasJavadocTag(Element el, DocTree.Kind kind) {
+ DocCommentTree tree = docConverter.getJavadocTree(el);
+ if (tree == null) {
+ return false;
+ }
+
+ Optional> res = tree.getFullBody()
+ .stream()
+ .map(DocTree::getKind)
+ .filter(kind::equals)
+ .findFirst();
+
+ if (res.isPresent()) {
+ return true;
+ }
+
+ return tree.getBlockTags()
+ .stream()
+ .map(DocTree::getKind)
+ .filter(kind::equals)
+ .findFirst()
+ .isPresent();
+ }
+
+ /**
+ * Adds the provided package to the set of processed packages
+ *
+ * This will create any additional required namespace packages
+ *
+ * @param pkg the package being processed
+ */
+ void addProcessedPackage(PackageElement pkg) {
+ String name = pkg.getQualifiedName().toString();
+ addProcessedPackage(PythonTypeStubElement.sanitizeQualifiedName(name));
+ }
+
+ /**
+ * Checks if the properties or ghidra options have been enabled
+ *
+ * @return true if either options are enabled
+ */
+ boolean isUsingPythonProperties() {
+ return useProperties;
+ }
+
+ /**
+ * Gets an appropriate string literal for the provided value
+ *
+ * The resulting String contains the value as required to be used in Java source code
+ *
+ * @param value the constant value
+ * @return an appropriate String literal for the constant value
+ */
+ String getStringLiteral(Object value) {
+ return docEnv.getElementUtils().getConstantExpression(value);
+ }
+
+ /**
+ * Checks if the provided package is included in the doclet output
+ *
+ * @param el the package element
+ * @return true if the package is included
+ */
+ private boolean isIncluded(PackageElement el) {
+ return docEnv.isIncluded(el);
+ }
+
+ /**
+ * Creates a namespace package for the provided package if one does not yet exist
+ *
+ * @param pkg the package to create
+ */
+ private void createNamespacePackage(String pkg) {
+ int index = pkg.indexOf('.');
+ if (index != -1) {
+ pkg = pkg.substring(0, index) + "-stubs" + pkg.substring(index);
+ }
+ else {
+ pkg += "-stubs";
+ }
+
+ File fp = new File(destDir, pkg.replace('.', '/') + "/__init__.pyi");
+ try {
+ fp.getParentFile().mkdirs();
+ fp.createNewFile();
+ }
+ catch (IOException e) {
+ // ignored
+ }
+ }
+
+ /**
+ * Adds the provided package to the set of processed packages
+ *
+ * A namespace package will be created if necessary
+ *
+ * @param pkg the package being processed
+ */
+ private void addProcessedPackage(String pkg) {
+ if (processedPackages.add(pkg)) {
+ createNamespacePackage(pkg);
+ int index = pkg.lastIndexOf('.');
+ if (index != -1) {
+ addProcessedPackage(pkg.substring(0, index));
+ }
+ else {
+ topLevelPackages.add(pkg);
+ }
+ }
+ }
+
+ /**
+ * Gets the docstring for the provided tags without markup
+ *
+ * @param tags the list of doclet tags
+ * @return the docstring without any markup
+ */
+ private static String getPlainDocString(List extends DocTree> tags) {
+ StringBuilder builder = new StringBuilder();
+ int ignoreDepth = 0;
+ for (DocTree tag : tags) {
+ switch (tag.getKind()) {
+ case LINK:
+ case LINK_PLAIN:
+ LinkTree link = (LinkTree) tag;
+ List extends DocTree> label = link.getLabel();
+ if (!label.isEmpty()) {
+ builder.append(getPlainDocString(label));
+ }
+ else {
+ String sig = link.getReference().getSignature().replaceAll("#", ".");
+ if (sig.startsWith(".")) {
+ sig = sig.substring(1);
+ }
+ builder.append(sig);
+ }
+ break;
+ case TEXT:
+ TextTree text = (TextTree) tag;
+ if (ignoreDepth == 0) {
+ builder.append(text.getBody());
+ }
+ break;
+ case START_ELEMENT:
+ StartElementTree start = (StartElementTree) tag;
+ if (!start.isSelfClosing()) {
+ ignoreDepth++;
+ }
+ break;
+ case END_ELEMENT:
+ ignoreDepth--;
+ break;
+ default:
+ break;
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubElement.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubElement.java
new file mode 100644
index 0000000000..954f8b1ec8
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubElement.java
@@ -0,0 +1,428 @@
+package ghidra.doclets.typestubs;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+
+/**
+ * Base class providing access to sanitized names (Python safe).
+ */
+abstract class PythonTypeStubElement {
+
+ private static final Set PY_KEYWORDS = new HashSet<>(
+ Set.of("False", "None", "True", "and", "as", "assert", "async", "await", "break",
+ "class", "continue", "def", "del", "elif", "else", "except", "exec", "finally", "for",
+ "from", "global", "if", "import", "in", "is", "lambda",
+ "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
+ "yield"));
+
+ static final String DOC_QUOTES = "\"\"\"";
+ static final String ALT_DOC_QUOTES = "'''";
+ static final String PY_INDENT = " ";
+
+ final PythonTypeStubDoclet doclet;
+ final T el;
+ private final PackageElement pkg;
+
+ private String name;
+
+ PythonTypeStubElement(PythonTypeStubDoclet doclet, T el) {
+ this(doclet, getPackage(el), el);
+ }
+
+ PythonTypeStubElement(PythonTypeStubDoclet doclet, PackageElement pkg, T el) {
+ this.doclet = doclet;
+ this.pkg = pkg;
+ this.el = el;
+ }
+
+ /**
+ * Gets the package for the provided element
+ *
+ * @param el the element
+ * @return the package
+ */
+ static PackageElement getPackage(Element el) {
+ while (!(el instanceof PackageElement)) {
+ el = el.getEnclosingElement();
+ }
+ return (PackageElement) el;
+ }
+
+ static int compareQualifiedNameable(QualifiedNameable a, QualifiedNameable b) {
+ return a.getQualifiedName().toString().compareTo(b.getQualifiedName().toString());
+ }
+
+ /**
+ * Checks if the provided element is in the same package as this element
+ *
+ * @param el the other element
+ * @return true if the other element is declared in the same package
+ */
+ boolean isSamePackage(Element el) {
+ return pkg.equals(getPackage(el));
+ }
+
+ /**
+ * Checks if the provided type is in the same package as this element
+ *
+ * @param type the type
+ * @return true if the type is declared in the same package
+ */
+ boolean isSamePackage(TypeMirror type) {
+ if (type instanceof DeclaredType dt) {
+ return pkg.equals(getPackage(dt.asElement()));
+ }
+ return false;
+ }
+
+ /**
+ * Gets the type string for the provided type and quotes if necessary
+ *
+ * This string value is safe to be used as a parameter or return type
+ * as well as for use in a generic type.
+ *
+ * @param self the type to become typing.Self if encountered
+ * @param type the type to get the string for
+ * @return the type string
+ */
+ String getTypeString(Element self, TypeMirror type) {
+ String typeName = sanitizeQualifiedName(self, type);
+ if (isSamePackage(type) && !typeName.equals("typing.Self")) {
+ typeName = '"' + typeName + '"';
+ }
+ return typeName;
+ }
+
+ /**
+ * Gets the Python safe name for this element
+ *
+ * @return the python safe name
+ */
+ final String getName() {
+ if (name == null) {
+ name = sanitize(el.getSimpleName());
+ }
+ return name;
+ }
+
+ /**
+ * Writes the Javadoc for the provided element to the provided printer
+ *
+ * @param element the element to write the javadoc for
+ * @param printer the printer to write to
+ * @param indent the indentation
+ * @param emptyValue the value to use when there is no documentation
+ * @return true if a Javadoc was written else false
+ */
+ final boolean writeJavaDoc(Element element, PrintWriter printer, String indent,
+ String emptyValue) {
+ String doc = doclet.getJavadoc(element);
+ if (doc.isBlank()) {
+ if (!emptyValue.isBlank()) {
+ printer.print(indent);
+ printer.print(emptyValue);
+ }
+ return false;
+ }
+ String quotes = doc.contains(DOC_QUOTES) ? ALT_DOC_QUOTES : DOC_QUOTES;
+ if (quotes == ALT_DOC_QUOTES) {
+ // ensure there are no problems
+ doc = doc.replaceAll(ALT_DOC_QUOTES, '\\' + ALT_DOC_QUOTES);
+ }
+ printer.print(indent);
+ printer.println(quotes);
+ writeLines(printer, doc.stripTrailing(), indent);
+ printer.print(indent);
+ printer.println(quotes);
+ return true;
+ }
+
+ /**
+ * Writes the Javadoc for this element to the provided printer
+ *
+ * @param printer the printer to write to
+ * @param indent the indentation
+ * @param emptyValue the value to use when there is no documentation
+ */
+ final void writeJavaDoc(PrintWriter printer, String indent, String emptyValue) {
+ writeJavaDoc(el, printer, indent, emptyValue);
+ }
+
+ /**
+ * Writes the Javadoc for this element to the provided printer
+ *
+ * @param printer the printer to write to
+ * @param indent the indentation
+ */
+ final void writeJavaDoc(PrintWriter printer, String indent) {
+ writeJavaDoc(el, printer, indent, "");
+ }
+
+ /**
+ * Makes the provided String Python safe if necessary
+ *
+ * @param value the value to make Python safe
+ * @return the Python safe value
+ */
+ static String sanitize(String value) {
+ if (PY_KEYWORDS.contains(value)) {
+ return value + "_";
+ }
+ return value;
+ }
+
+ /**
+ * Makes the provided element name Python safe if necessary
+ *
+ * @param name the name to make Python safe
+ * @return the Python safe name
+ */
+ static String sanitize(Name name) {
+ return sanitize(name.toString());
+ }
+
+ /**
+ * Makes the provided qualified name Python safe if necessary
+ *
+ * @param name the qualified name to make Python safe
+ * @return the Python safe qualified name
+ */
+ static String sanitizeQualifiedName(String name) {
+ Iterator it = Arrays.stream(name.split("\\."))
+ .map(PythonTypeStubElement::sanitize)
+ .iterator();
+ return String.join(".", (Iterable) () -> it);
+ }
+
+ /**
+ * Makes the provided qualified name Python safe if necessary
+ *
+ * @param name the qualified name to make Python safe
+ * @return the Python safe qualified name
+ */
+ static String sanitizeQualifiedName(QualifiedNameable name) {
+ return sanitizeQualifiedName(name.getQualifiedName().toString());
+ }
+
+ /**
+ * Makes the provided package name Python safe if necessary
+ *
+ * @param pkg the package to make Python safe
+ * @return the Python safe package name
+ */
+ static String sanitizeQualifiedName(PackageElement pkg) {
+ return sanitizeQualifiedName(pkg.getQualifiedName().toString());
+ }
+
+ /**
+ * Makes the provided type Python safe if necessary
+ *
+ * @param self the type to become typing.Self if encountered
+ * @param type the type to make Python safe
+ * @param pkg the current package
+ * @return the Python safe type name
+ */
+ static String sanitize(Element self, TypeMirror type, PackageElement pkg) {
+ return switch (type.getKind()) {
+ case DECLARED -> throw new RuntimeException(
+ "declared types should use the qualified name");
+ case ARRAY -> {
+ TypeMirror component = ((ArrayType) type).getComponentType();
+ yield "jpype.JArray[" + sanitizeQualifiedName(self, component, pkg) + "]";
+ }
+ case BOOLEAN -> "jpype.JBoolean";
+ case BYTE -> "jpype.JByte";
+ case CHAR -> "jpype.JChar";
+ case DOUBLE -> "jpype.JDouble";
+ case FLOAT -> "jpype.JFloat";
+ case INT -> "jpype.JInt";
+ case LONG -> "jpype.JLong";
+ case SHORT -> "jpype.JShort";
+ case TYPEVAR -> type.toString();
+ case WILDCARD -> getWildcardVarName(self, (WildcardType) type, pkg);
+ default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
+ };
+ }
+
+ /**
+ * Checks if the provided type is the same as the provided element
+ *
+ * @param self the element of the type to become typing.Self
+ * @param type the type to check
+ * @return true if the inputs represent the same type
+ */
+ static final boolean isSelfType(Element self, TypeMirror type) {
+ if (self.getKind() == ElementKind.ENUM) {
+ // typing.Self is usually invalid here
+ return false;
+ }
+ if (type instanceof DeclaredType dt) {
+ return self.equals(dt.asElement());
+ }
+ return false;
+ }
+
+ /**
+ * Makes the qualified name for the provided type Python safe if necessary
+ *
+ * @param self the type to become typing.Self if encountered
+ * @param type the type to make Python safe
+ * @return the Python safe qualified type name
+ */
+ final String sanitizeQualifiedName(Element self, TypeMirror type) {
+ return sanitizeQualifiedName(self, type, pkg);
+ }
+
+ /**
+ * Makes the qualified name for the provided type Python safe if necessary
+ *
+ * The provided package is used to check each type and generic components.
+ * If they require a "forward declaration", it is handled accordingly.
+ *
+ * @param self the type to become typing.Self if encountered
+ * @param type the type to make Python safe
+ * @param pkg the current package
+ * @return the Python safe qualified type name
+ */
+ static final String sanitizeQualifiedName(Element self, TypeMirror type, PackageElement pkg) {
+ if (isSelfType(self, type)) {
+ return "typing.Self";
+ }
+ if (type instanceof DeclaredType dt) {
+ TypeElement el = (TypeElement) dt.asElement();
+ PackageElement typePkg = getPackage(el);
+
+ String name;
+ if (pkg.equals(typePkg)) {
+ name = sanitize(el.getSimpleName());
+ Element parent = el.getEnclosingElement();
+ while (parent instanceof TypeElement parentType) {
+ parent = parent.getEnclosingElement();
+ name = sanitize(parentType.getSimpleName()) + "." + name;
+ }
+ }
+ else {
+ name = sanitizeQualifiedName(el);
+ }
+
+ List extends TypeMirror> args = dt.getTypeArguments();
+ if (args.isEmpty()) {
+ return name;
+ }
+ Iterable it = () -> args.stream()
+ .map(paramType -> sanitizeQualifiedName(self, paramType, pkg))
+ .iterator();
+ return name + "[" + String.join(", ", it) + "]";
+ }
+ return sanitize(self, type, pkg);
+ }
+
+ /**
+ * Checks if the provided element is static
+ *
+ * @param el the element to check
+ * @return true if the element is static
+ */
+ static boolean isStatic(Element el) {
+ return el.getModifiers().contains(Modifier.STATIC);
+ }
+
+ /**
+ * Checks if the provided element is final
+ *
+ * @param el the element to check
+ * @return true if the element is final
+ */
+ static boolean isFinal(Element el) {
+ return el.getModifiers().contains(Modifier.FINAL);
+ }
+
+ /**
+ * Checks if the provided element is public
+ *
+ * @param el the element to check
+ * @return true if the element is public
+ */
+ static boolean isPublic(Element el) {
+ return el.getModifiers().contains(Modifier.PUBLIC);
+ }
+
+ /**
+ * Checks if the provided element is protected
+ *
+ * @param el the element to check
+ * @return true if the element is protected
+ */
+ static boolean isProtected(Element el) {
+ return el.getModifiers().contains(Modifier.PROTECTED);
+ }
+
+ /**
+ * Increases the provided indentation by one level
+ *
+ * @param indent the indentation
+ * @return the new indentation
+ */
+ static String indent(String indent) {
+ return indent + PY_INDENT;
+ }
+
+ /**
+ * Decreases the provided indentation by one level
+ *
+ * @param indent the indentation
+ * @return the new indentation
+ */
+ static String deindent(String indent) {
+ return indent.substring(0, indent.length() - PY_INDENT.length());
+ }
+
+ /**
+ * Gets the name for a wildcard type if possible
+ *
+ * @param self the type to become typing.Self if encountered
+ * @param type the wildcard type
+ * @param pkg the current package
+ * @return the determined type name if possible otherwise typing.Any
+ */
+ private static String getWildcardVarName(Element self, WildcardType type, PackageElement pkg) {
+ TypeMirror base = type.getExtendsBound();
+ if (base == null) {
+ base = type.getSuperBound();
+ }
+ if (base != null) {
+ return sanitizeQualifiedName(self, base, pkg);
+ }
+ return "typing.Any";
+ }
+
+ /**
+ * Writes the lines to the printer with the provided intentation
+ *
+ * @param printer the printer
+ * @param lines the lines to write
+ * @param indent the indentation to use
+ */
+ private static void writeLines(PrintWriter printer, String lines, String indent) {
+ lines.lines().forEach((line) -> {
+ printer.print(indent);
+ printer.println(line);
+ });
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubMethod.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubMethod.java
new file mode 100644
index 0000000000..5509819aa3
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubMethod.java
@@ -0,0 +1,499 @@
+package ghidra.doclets.typestubs;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.QualifiedNameable;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.ArrayType;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.ExecutableType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * {@link PythonTypeStubElement} for a method
+ */
+final class PythonTypeStubMethod extends PythonTypeStubElement
+ implements Comparable {
+
+ private static final String EMPTY_DOCS = "..." + System.lineSeparator();
+
+ private static final Map AUTO_CONVERSIONS = new HashMap<>(
+ Map.ofEntries(
+ Map.entry("java.lang.Boolean", "typing.Union[java.lang.Boolean, bool]"),
+ Map.entry("java.lang.Byte", "typing.Union[java.lang.Byte, int]"),
+ Map.entry("java.lang.Character", "typing.Union[java.lang.Character, int, str]"),
+ Map.entry("java.lang.Double", "typing.Union[java.lang.Double, float]"),
+ Map.entry("java.lang.Float", "typing.Union[java.lang.Float, float]"),
+ Map.entry("java.lang.Integer", "typing.Union[java.lang.Integer, int]"),
+ Map.entry("java.lang.Long", "typing.Union[java.lang.Long, int]"),
+ Map.entry("java.lang.Short", "typing.Union[java.lang.Short, int]"),
+ Map.entry("java.lang.String", "typing.Union[java.lang.String, str]"),
+ Map.entry("java.io.File", "jpype.protocol.SupportsPath"),
+ Map.entry("java.nio.file.Path", "jpype.protocol.SupportsPath"),
+ Map.entry("java.lang.Iterable", "collections.abc.Sequence"),
+ Map.entry("java.util.Collection", "collections.abc.Sequence"),
+ Map.entry("java.util.Map", "collections.abc.Mapping"),
+ Map.entry("java.time.Instant", "datetime.datetime"),
+ Map.entry("java.sql.Time", "datetime.time"),
+ Map.entry("java.sql.Date", "datetime.date"),
+ Map.entry("java.sql.Timestamp", "datetime.datetime"),
+ Map.entry("java.math.BigDecimal", "decimal.Decimal")));
+
+ // FIXME: list and set aren't automatically converted to java.util.List and java.util.Set :(
+ // if wanted they could be setup to be converted automatically by pyhidra
+ // however, when passed as a parameter and modified, the original underlyng python container
+ // wouldn't be modified. To make it work as expected, a python implementation for
+ // java.util.List and java.util.Set would need to be created using jpype.JImplements,
+ // that would wrap the list/set before passing it to Java instead of copying the contents
+ // into a Java List/Set.
+
+ private static final Map RESULT_CONVERSIONS = new HashMap<>(
+ Map.of(
+ "java.lang.Boolean", "bool",
+ "java.lang.Byte", "int",
+ "java.lang.Character", "str",
+ "java.lang.Double", "float",
+ "java.lang.Float", "float",
+ "java.lang.Integer", "int",
+ "java.lang.Long", "int",
+ "java.lang.Short", "int",
+ "java.lang.String", "str"));
+
+ private final PythonTypeStubType parent;
+ private final boolean filterSelf;
+ List typevars;
+ Set imports;
+
+ /**
+ * Creates a new {@link PythonTypeStubMethod}
+ *
+ * @param parent the type containing this method
+ * @param el the element for this method
+ */
+ PythonTypeStubMethod(PythonTypeStubType parent, ExecutableElement el) {
+ this(parent, el, false);
+ }
+
+ /**
+ * Creates a new {@link PythonTypeStubMethod}
+ *
+ * @param parent the type containing this method
+ * @param el the element for this method
+ * @param filterSelf true if the self parameter should be filtered
+ */
+ PythonTypeStubMethod(PythonTypeStubType parent, ExecutableElement el, boolean filterSelf) {
+ super(parent.doclet, el);
+ this.parent = parent;
+ this.filterSelf = filterSelf;
+ }
+
+ /**
+ * Processes the method and prints it to the provided printer
+ *
+ * @param printer the printer
+ * @param indent the indentation
+ * @param overload true if the overload annotation should be applied
+ */
+ void process(PrintWriter printer, String indent, boolean overload) {
+ String name = sanitize(getName());
+ Set modifiers = el.getModifiers();
+ boolean isStatic = modifiers.contains(Modifier.STATIC);
+
+ if (name.equals("")) {
+ name = "__init__";
+ }
+
+ printer.print(indent);
+ if (isStatic) {
+ printer.println("@staticmethod");
+ printer.print(indent);
+ }
+
+ if (overload) {
+ printer.println("@typing.overload");
+ printer.print(indent);
+ }
+
+ if (doclet.isDeprecated(el)) {
+ String msg = doclet.getDeprecatedMessage(el);
+ if (msg != null) {
+ // a message is required
+ // if one is not present, don't apply it
+ printer.print("@deprecated(");
+ printer.print(msg);
+ printer.println(')');
+ printer.print(indent);
+ }
+ }
+
+ printer.print("def ");
+ printer.print(name);
+
+ printSignature(printer, filterSelf || isStatic);
+
+ printer.println(":");
+ indent += PY_INDENT;
+ writeJavaDoc(el, printer, indent, EMPTY_DOCS);
+ printer.println();
+ }
+
+ /**
+ * Gets a collection of all TypeVars needed by this method
+ *
+ * @return a collection of all needed TypeVars
+ */
+ Collection getTypeVars() {
+ if (typevars != null) {
+ return typevars;
+ }
+
+ List extends TypeParameterElement> params = el.getTypeParameters();
+ typevars = new ArrayList<>(params.size());
+ for (TypeParameterElement param : params) {
+ typevars.add(param.getSimpleName().toString());
+ }
+ return typevars;
+ }
+
+ /**
+ * Gets a collection of all type that need to be imported for this method
+ *
+ * @return a collection of types to import
+ */
+ Collection getImportedTypes() {
+ if (imports != null) {
+ return imports;
+ }
+
+ List extends VariableElement> parameters = el.getParameters();
+ TypeMirror resType = el.getReturnType();
+
+ // make the set big enough for all paramters and the return type
+ imports = new HashSet<>(parameters.size() + 1);
+
+ if (resType instanceof DeclaredType dt) {
+ imports.add((TypeElement) dt.asElement());
+ }
+
+ for (VariableElement param : parameters) {
+ if (param.asType() instanceof DeclaredType dt) {
+ imports.add((TypeElement) dt.asElement());
+ }
+ }
+
+ return imports;
+ }
+
+ /**
+ * Converts the result type to the Python equivalent type if applicable
+ *
+ * @param type the result type
+ * @return the Python equivalent type or null if there is no equivalent type
+ */
+ static String convertResultType(TypeMirror type) {
+ if (type.getKind().isPrimitive()) {
+ return switch (type.getKind()) {
+ case BOOLEAN -> "bool";
+ case BYTE -> "int";
+ case CHAR -> "str";
+ case DOUBLE -> "float";
+ case FLOAT -> "float";
+ case INT -> "int";
+ case LONG -> "int";
+ case SHORT -> "int";
+ default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
+ };
+ }
+
+ if (type instanceof DeclaredType dt) {
+ Element element = dt.asElement();
+ if (element instanceof QualifiedNameable nameable) {
+ return RESULT_CONVERSIONS.get(nameable.getQualifiedName().toString());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if this method is a candidate for a Python property
+ *
+ * @return true if this method may be a Python property
+ */
+ boolean isProperty() {
+ if (isStatic(el)) {
+ return false;
+ }
+
+ List extends VariableElement> params = el.getParameters();
+ if (params.size() > 1) {
+ return false;
+ }
+
+ String name = getName();
+ TypeKind resultKind = getReturnType().getKind();
+ try {
+ if (name.startsWith("get")) {
+ return Character.isUpperCase(name.charAt(3)) && resultKind != TypeKind.VOID;
+ }
+ if (name.startsWith("is")) {
+ return Character.isUpperCase(name.charAt(2)) && resultKind != TypeKind.VOID;
+ }
+ if (name.startsWith("set")) {
+ if (params.size() != 1) {
+ return false;
+ }
+ return Character.isUpperCase(name.charAt(3)) && resultKind == TypeKind.VOID;
+ }
+ }
+ catch (IndexOutOfBoundsException e) {
+ // name check failed
+ }
+ return false;
+ }
+
+ /**
+ * Converts this method to its Python property form
+ *
+ * @return this method as a Python property
+ */
+ PropertyMethod asProperty() {
+ return new PropertyMethod();
+ }
+
+ /**
+ * Prints the Python equivalent method signature to the provided printer
+ *
+ * @param printer the printer
+ * @param isStatic true if this method is a static method
+ */
+ private void printSignature(PrintWriter printer, boolean isStatic) {
+ List names = getParameterNames();
+ List extends TypeMirror> types = getParameterTypes();
+ StringBuilder args = new StringBuilder();
+
+ if (!isStatic) {
+ args.append("self");
+ }
+
+ for (int i = 0; i < names.size(); i++) {
+ if (i != 0 || !isStatic) {
+ args.append(", ");
+ }
+ if (el.isVarArgs() && i == names.size() - 1) {
+ ArrayType type = (ArrayType) types.get(i);
+ String arg = convertParam(names.get(i), type.getComponentType());
+ args.append('*' + arg);
+ }
+ else {
+ args.append(convertParam(names.get(i), types.get(i)));
+ }
+ }
+
+ printer.print("(");
+ printer.print(args);
+ printer.print(")");
+
+ TypeMirror res = el.getReturnType();
+ if (res.getKind() != TypeKind.VOID) {
+ printer.print(" -> ");
+ String convertedType = convertResultType(res);
+ if (convertedType != null) {
+ printer.print(convertedType);
+ }
+ else {
+ printer.print(getTypeString(parent.el, res));
+ }
+ }
+ }
+
+ /**
+ * Gets the property name for this method if applicable
+ *
+ * @return the property name or null
+ */
+ private String getPropertyName() {
+ String name = getName();
+ if (name.startsWith("get") || name.startsWith("set")) {
+ return Character.toLowerCase(name.charAt(3)) + name.substring(4);
+ }
+ if (name.startsWith("is")) {
+ return Character.toLowerCase(name.charAt(2)) + name.substring(3);
+ }
+ return null;
+ }
+
+ /**
+ * Gets a list of all the parameter types
+ *
+ * @return the list of parameter types
+ */
+ private List extends TypeMirror> getParameterTypes() {
+ return ((ExecutableType) el.asType()).getParameterTypes();
+ }
+
+ /**
+ * Gets a list of all the Python safe parameter names
+ *
+ * @return the list of parameter names
+ */
+ private List getParameterNames() {
+ List extends VariableElement> params = el.getParameters();
+ List names = new ArrayList<>(params.size());
+ for (VariableElement param : params) {
+ String name = sanitize(param.getSimpleName());
+ if (name.equals("self")) {
+ name = "self_";
+ }
+ names.add(name);
+ }
+ return names;
+ }
+
+ /**
+ * Gets the return type
+ *
+ * @return the return type
+ */
+ private TypeMirror getReturnType() {
+ return el.getReturnType();
+ }
+
+ /**
+ * Converts the provided parameter type to a typing.Union of all the allowed types
+ *
+ * @param name the parameter name
+ * @param type the parameter type
+ * @return the parameter and its type
+ */
+ private String convertParam(String name, TypeMirror type) {
+ String convertedType = convertParamType(type);
+ if (convertedType != null) {
+ return name + ": " + convertedType;
+ }
+ return name + ": " + getTypeString(parent.el, type);
+ }
+
+ /**
+ * Converts the provided parameter type to a typing.Union of all the allowed types
+ *
+ * @param type the parameter type
+ * @return the converted type
+ */
+ private static String convertParamType(TypeMirror type) {
+ if (type.getKind().isPrimitive()) {
+ return switch (type.getKind()) {
+ case BOOLEAN -> "typing.Union[jpype.JBoolean, bool]";
+ case BYTE -> "typing.Union[jpype.JByte, int]";
+ case CHAR -> "typing.Union[jpype.JChar, int, str]";
+ case DOUBLE -> "typing.Union[jpype.JDouble, float]";
+ case FLOAT -> "typing.Union[jpype.JFloat, float]";
+ case INT -> "typing.Union[jpype.JInt, int]";
+ case LONG -> "typing.Union[jpype.JLong, int]";
+ case SHORT -> "typing.Union[jpype.JShort, int]";
+ default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
+ };
+ }
+
+ if (type instanceof DeclaredType dt) {
+ Element element = dt.asElement();
+ if (element instanceof QualifiedNameable nameable) {
+ return AUTO_CONVERSIONS.get(nameable.getQualifiedName().toString());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper for creating a Python property.
+ *
+ * This class only represents one part of a complete Python property.
+ */
+ class PropertyMethod {
+
+ /**
+ * Gets the name for this property
+ *
+ * @return the property name
+ */
+ String getName() {
+ return sanitize(getPropertyName());
+ }
+
+ /**
+ * Checks if this property is a getter
+ *
+ * @return true if this property is a getter
+ */
+ boolean isGetter() {
+ return el.getReturnType().getKind() != TypeKind.VOID;
+ }
+
+ /**
+ * Checks if this property is a setter
+ *
+ * @return true if this property is a setter
+ */
+ boolean isSetter() {
+ return el.getReturnType().getKind() == TypeKind.VOID;
+ }
+
+ /**
+ * Gets the type for this property
+ *
+ * @return the property type
+ */
+ TypeMirror getType() {
+ TypeMirror type;
+ if (isGetter()) {
+ type = el.getReturnType();
+ }
+ else {
+ type = getParameterTypes().get(0);
+ }
+ try {
+ return doclet.getTypeUtils().unboxedType(type);
+ }
+ catch (IllegalArgumentException e) {
+ // not boxed
+ return type;
+ }
+ }
+
+ /**
+ * Checks if this property and the other provided property form a pair
+ *
+ * @param other the other property
+ * @return true if the two properties form a pair
+ */
+ boolean isPair(PropertyMethod other) {
+ if (isGetter() && other.isGetter()) {
+ return false;
+ }
+ if (isSetter() && other.isSetter()) {
+ return false;
+ }
+ if (!getName().equals(other.getName())) {
+ return false;
+ }
+ return doclet.getTypeUtils().isSameType(getType(), other.getType());
+ }
+ }
+
+ @Override
+ public int compareTo(PythonTypeStubMethod other) {
+ return getName().compareTo(other.getName());
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubNestedType.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubNestedType.java
new file mode 100644
index 0000000000..9bcddfefc1
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubNestedType.java
@@ -0,0 +1,30 @@
+package ghidra.doclets.typestubs;
+
+import java.io.PrintWriter;
+
+import javax.lang.model.element.TypeElement;
+
+/**
+ * {@link PythonTypeStubElement} for a nested type
+ */
+final class PythonTypeStubNestedType extends PythonTypeStubType {
+
+ // while it is possible to create a pseudo sub module to
+ // make static nested classes and enum values individually
+ // importable during type checking, it's not worth the effort
+
+ /**
+ * Creates a new {@link PythonTypeStubNestedType}
+ *
+ * @param pkg the package containing this type
+ * @param el the element for this type
+ */
+ PythonTypeStubNestedType(PythonTypeStubPackage pkg, TypeElement el) {
+ super(pkg, el);
+ }
+
+ @Override
+ void process(PrintWriter printer, String indent) {
+ printClass(printer, indent);
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubPackage.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubPackage.java
new file mode 100644
index 0000000000..0852a83560
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubPackage.java
@@ -0,0 +1,242 @@
+package ghidra.doclets.typestubs;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+
+/**
+ * {@link PythonTypeStubElement} for a package
+ *
+ * This will process all visible classes, interfaces, handle necessary imports
+ * and create the __init__.pyi file.
+ */
+final class PythonTypeStubPackage extends PythonTypeStubElement {
+
+ private String packageName;
+ private File path;
+ private List types;
+
+ /**
+ * Creates a new {@link PythonTypeStubPackage}
+ *
+ * @param doclet the current doclet
+ * @param el the element for this package
+ */
+ PythonTypeStubPackage(PythonTypeStubDoclet doclet, PackageElement el) {
+ super(doclet, el);
+ }
+
+ /**
+ * Gets a list of all the TypeVars needed by the types in this package
+ *
+ * @return a list of all the needed TypeVars
+ */
+ List getTypeVars() {
+ Set typevars = new HashSet<>();
+ for (PythonTypeStubType type : getTypes()) {
+ typevars.addAll(type.getTypeVars());
+ }
+ List res = new ArrayList<>(typevars);
+ res.sort(null);
+ return res;
+ }
+
+ /**
+ * Gets a collection of all the imported types needed by the types in this package
+ *
+ * @return a collection of all the imported types
+ */
+ Collection getImportedTypes() {
+ Set imported = new HashSet<>();
+ for (PythonTypeStubType type : getTypes()) {
+ imported.addAll(type.getImportedTypes());
+ }
+ return imported;
+ }
+
+ /**
+ * Gets the Python safe, fully qualified name for this package
+ *
+ * @return the qualified package name
+ */
+ String getPackageName() {
+ if (packageName == null) {
+ packageName = sanitizeQualifiedName(el.getQualifiedName().toString());
+ }
+ return packageName;
+ }
+
+ /**
+ * Processes this package and its contents to create a __init__.pyi file
+ */
+ void process() {
+ doclet.addProcessedPackage(el);
+ getPath().mkdirs();
+ File stub = new File(path, "__init__.pyi");
+ try (PrintWriter printer = new PrintWriter(new FileWriter(stub))) {
+ process(printer, "");
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Gets a list of all the types declared in this package
+ *
+ * @return a list of all specified types
+ */
+ List getTypes() {
+ // NOTE: do ALL SPECIFIED TYPES
+ // if it is not public, it will be decorated with @typing.type_check_only
+ // this prevents errors during typechecking from having a class with a base
+ // class that doesn't have public visibility
+ if (types != null) {
+ return types;
+ }
+ types = new ArrayList<>();
+ for (Element child : el.getEnclosedElements()) {
+ switch (child.getKind()) {
+ case CLASS:
+ case INTERFACE:
+ case ENUM:
+ case RECORD:
+ if (!doclet.isSpecified(child)) {
+ continue;
+ }
+ types.add(new PythonTypeStubType(this, (TypeElement) child));
+ break;
+ default:
+ break;
+ }
+ }
+ return types;
+ }
+
+ /**
+ * Process the contents of this package and write the results to the provided printer
+ *
+ * @param printer the printer to write to
+ * @param indent the current indentation
+ */
+ private void process(PrintWriter printer, String indent) {
+ writeJavaDoc(printer, indent, "");
+ printer.println("import collections.abc");
+ printer.println("import datetime");
+ printer.println("import typing");
+ printer.println("from warnings import deprecated # type: ignore");
+ printer.println();
+ printer.println("import jpype # type: ignore");
+ printer.println("import jpype.protocol # type: ignore");
+ printer.println();
+ doclet.printImports(printer, getImportedPackages());
+ printer.println();
+ printer.println();
+ printTypeVars(printer);
+ Set exports = new LinkedHashSet<>();
+ for (PythonTypeStubType type : getTypes()) {
+ processType(printer, indent, type);
+ exports.add('"' + type.getName() + '"');
+ }
+ printer.println();
+
+ // create the __all__ variable to prevent our imports and TypeVars from being
+ // imported when "from {getPackageName()} import *" is used
+ printer.print("__all__ = [");
+ printer.print(String.join(", ", exports));
+ printer.println("]");
+ }
+
+ /**
+ * Gets the output directory for this package
+ *
+ * @return the output directory
+ */
+ private File getPath() {
+ if (path == null) {
+ String name = getPackageName();
+ int index = name.indexOf('.');
+ if (index != -1) {
+ name = name.substring(0, index) + "-stubs" + name.substring(index);
+ }
+ else {
+ name += "-stubs";
+ }
+ path = new File(doclet.getDestDir(), name.replace('.', '/'));
+ }
+ return path;
+ }
+
+ /**
+ * Gets a collection of all imported packages
+ *
+ * @return a collection of all imported packages
+ */
+ private Collection getImportedPackages() {
+ Set packages = new HashSet<>();
+ for (TypeElement element : getImportedTypes()) {
+ if (isNestedType(element)) {
+ // don't import types declared in this file
+ continue;
+ }
+
+ PackageElement importedPkg = getPackage(element);
+ if (importedPkg == null || el.equals(importedPkg)) {
+ continue;
+ }
+ packages.add(importedPkg);
+ }
+
+ List res = new ArrayList<>(packages);
+ res.sort(PythonTypeStubElement::compareQualifiedNameable);
+ return res;
+ }
+
+ /**
+ * Processes the provided type and write it to the provided printer
+ *
+ * @param printer the printer
+ * @param indent the current indentation
+ * @param type the type
+ */
+ private void processType(PrintWriter printer, String indent, PythonTypeStubType type) {
+ type.process(printer, indent);
+ }
+
+ /**
+ * Checks if the provided type is a nested type
+ *
+ * @param element the type element to check
+ * @return true if the type is declared within another class
+ */
+ private static boolean isNestedType(TypeElement element) {
+ return element.getEnclosingElement() instanceof TypeElement;
+ }
+
+ /**
+ * Prints all the typevars to the provided printer
+ *
+ * @param printer the printer
+ */
+ private void printTypeVars(PrintWriter printer) {
+ List allTypeVars = getTypeVars();
+ for (String generic : allTypeVars) {
+ printer.println(generic + " = typing.TypeVar(\"" + generic + "\")");
+ }
+ if (!allTypeVars.isEmpty()) {
+ printer.println();
+ printer.println();
+ }
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubType.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubType.java
new file mode 100644
index 0000000000..927425c062
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/PythonTypeStubType.java
@@ -0,0 +1,715 @@
+package ghidra.doclets.typestubs;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+
+import com.sun.source.doctree.DocTree;
+
+/**
+ * {@link PythonTypeStubElement} for a declared type
+ */
+class PythonTypeStubType extends PythonTypeStubElement {
+
+ private static final String OBJECT_NAME = Object.class.getName();
+ private static final Map GENERIC_CUSTOMIZERS = new HashMap<>(Map.ofEntries(
+ Map.entry("java.lang.Iterable", "collections.abc.Iterable"),
+ Map.entry("java.util.Collection", "collections.abc.Collection"),
+ Map.entry("java.util.List", "list"),
+ Map.entry("java.util.Map", "dict"),
+ Map.entry("java.util.Set", "set"),
+ Map.entry("java.util.Map.Entry", "tuple"),
+ Map.entry("java.util.Iterator", "collections.abc.Iterator"),
+ Map.entry("java.util.Enumeration", "collections.abc.Iterator")));
+
+ private final PythonTypeStubPackage pkg;
+ private Set imports;
+ private Set typevars;
+ private List nestedTypes;
+ private List fields;
+ private List methods;
+ private List properties;
+ private Set fieldNames;
+ private Set methodNames;
+
+ /**
+ * Creates a new {@link PythonTypeStubType}
+ *
+ * @param pkg the package containing this type
+ * @param el the element for this type
+ */
+ PythonTypeStubType(PythonTypeStubPackage pkg, TypeElement el) {
+ super(pkg.doclet, pkg.el, el);
+ this.pkg = pkg;
+ }
+
+ /**
+ * Process the current type and write it to the provided printer
+ *
+ * @param printer the printer
+ * @param indent the indentation
+ */
+ void process(PrintWriter printer, String indent) {
+ printClass(printer, indent);
+ }
+
+ /**
+ * Gets a set of all the TypeVars used by this type
+ *
+ * @return a set of all used TypeVars
+ */
+ Set getTypeVars() {
+ if (typevars != null) {
+ return typevars;
+ }
+ List extends TypeParameterElement> params = el.getTypeParameters();
+ typevars = new HashSet<>();
+ for (TypeParameterElement param : params) {
+ typevars.add(param.getSimpleName().toString());
+ }
+ for (PythonTypeStubNestedType nested : getNestedTypes()) {
+ typevars.addAll(nested.getTypeVars());
+ }
+ for (PythonTypeStubMethod method : getMethods()) {
+ typevars.addAll(method.getTypeVars());
+ }
+ return typevars;
+ }
+
+ /**
+ * Gets a collection of all the imported types used by this type
+ *
+ * @return a collection of all imported types
+ */
+ final Collection getImportedTypes() {
+ if (imports != null) {
+ return imports;
+ }
+ imports = new HashSet<>();
+ TypeMirror base = el.getSuperclass();
+ if (base instanceof DeclaredType dt) {
+ imports.add((TypeElement) dt.asElement());
+ }
+ for (TypeMirror iface : el.getInterfaces()) {
+ if (iface instanceof DeclaredType dt) {
+ imports.add((TypeElement) dt.asElement());
+ }
+ }
+ for (PythonTypeStubNestedType nested : getNestedTypes()) {
+ imports.addAll(nested.getImportedTypes());
+ }
+ for (VariableElement field : getFields()) {
+ TypeMirror fieldType = field.asType();
+ if (fieldType instanceof DeclaredType dt) {
+ imports.add((TypeElement) dt.asElement());
+ }
+ }
+ for (PythonTypeStubMethod method : getMethods()) {
+ imports.addAll(method.getImportedTypes());
+ }
+ return imports;
+ }
+
+ /**
+ * Gets a list of all the nested types declared in this type
+ *
+ * @return a list of all nested types
+ */
+ final List getNestedTypes() {
+ if (nestedTypes != null) {
+ return nestedTypes;
+ }
+ nestedTypes = new ArrayList<>();
+ for (Element child : el.getEnclosedElements()) {
+ if (child instanceof TypeElement type) {
+ nestedTypes.add(new PythonTypeStubNestedType(pkg, type));
+ }
+ }
+ return nestedTypes;
+ }
+
+ /**
+ * Gets a list of all the public fields in this type
+ *
+ * @return a list of all public fields
+ */
+ final List getFields() {
+ return getFields(false);
+ }
+
+ /**
+ * Gets a list of all the visible fields in this type
+ *
+ * @param protectedScope true to include protected fields
+ * @return a list of all visible fields
+ */
+ final List getFields(boolean protectedScope) {
+ if (fields != null) {
+ return fields;
+ }
+ fields = new ArrayList<>();
+ for (Element child : el.getEnclosedElements()) {
+ switch (child.getKind()) {
+ case ENUM_CONSTANT:
+ case FIELD:
+ break;
+ default:
+ continue;
+ }
+ if (!isVisible(child, protectedScope)) {
+ continue;
+ }
+ fields.add((VariableElement) child);
+ }
+ return fields;
+ }
+
+ /**
+ * Gets a list of all public methods and constructors in this type
+ *
+ * @return a list of all public methods
+ */
+ final List getMethods() {
+ return getMethods(false, false);
+ }
+
+ /**
+ * Gets a list of all visible methods in this type
+ *
+ * @param protectedScope true to include protected methods
+ * @param filterConstructor true to filter constructors
+ * @return a list of visible methods
+ */
+ final List getMethods(boolean protectedScope, boolean filterConstructor) {
+ if (methods != null) {
+ return methods;
+ }
+ methods = new ArrayList<>();
+ for (Element child : el.getEnclosedElements()) {
+ switch (child.getKind()) {
+ case CONSTRUCTOR:
+ if (filterConstructor) {
+ continue;
+ }
+ case METHOD:
+ if (!isVisible(child, protectedScope)) {
+ continue;
+ }
+ if (isUndocumentedOverride(child)) {
+ continue;
+ }
+ methods.add(new PythonTypeStubMethod(this, (ExecutableElement) child,
+ filterConstructor));
+ break;
+ default:
+ break;
+ }
+
+ }
+ // apparently overloads must come one after another
+ // therefore this must be sorted
+ methods.sort(null);
+ return methods;
+ }
+
+ /**
+ * Checks if the provided method needs the typing.overload decorator
+ *
+ * @param methods the list of methods
+ * @param it the current iterator
+ * @param method the method to check
+ * @return true if typing.overload should be applied
+ */
+ static boolean isOverload(List methods,
+ ListIterator it, PythonTypeStubMethod method) {
+ if (it.hasNext()) {
+ if (methods.get(it.nextIndex()).getName().equals(method.getName())) {
+ return true;
+ }
+ }
+ int index = it.previousIndex();
+ if (index >= 1) {
+ // the previous index is actually the index of the method parameter
+ if (methods.get(index - 1).getName().equals(method.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Prints the Python class definition for this type to the provided printer
+ *
+ * @param printer the printer
+ * @param indent the current indentation
+ */
+ final void printClass(PrintWriter printer, String indent) {
+ printClassDefinition(printer, indent);
+ indent = indent(indent);
+ for (PythonTypeStubNestedType nested : getNestedTypes()) {
+ nested.process(printer, indent);
+ }
+ for (VariableElement field : getFields()) {
+ printField(field, printer, indent, isStatic(field));
+ }
+ if (!getFields().isEmpty()) {
+ printer.println();
+ }
+ ListIterator methodIterator = getMethods().listIterator();
+ while (methodIterator.hasNext()) {
+ PythonTypeStubMethod method = methodIterator.next();
+ boolean overload = isOverload(getMethods(), methodIterator, method);
+ method.process(printer, indent, overload);
+ }
+ if (!doclet.isUsingPythonProperties()) {
+ printer.println();
+ return;
+ }
+ for (Property property : getProperties()) {
+ property.process(printer, indent);
+ }
+ printer.println();
+ }
+
+ /**
+ * Prints the provided field to the provided printer
+ *
+ * @param field the field to print
+ * @param printer the printer
+ * @param indent the indentation
+ * @param isStatic true if the field is static
+ */
+ void printField(VariableElement field, PrintWriter printer, String indent, boolean isStatic) {
+ String name = sanitize(field.getSimpleName());
+ printer.print(indent);
+ printer.print(name);
+
+ String value = getConstantValue(field);
+ if (value != null) {
+ // constants are always static final
+ printer.print(": typing.Final = ");
+ printer.println(value);
+ }
+ else {
+ TypeMirror type = field.asType();
+ printer.print(": ");
+ String sanitizedType = getTypeString(el, type);
+
+ // only one of these may be applied
+ // prefer Final over ClassVar
+ if (isFinal(field)) {
+ sanitizedType = applyFinal(sanitizedType);
+ }
+ else if (isStatic) {
+ sanitizedType = applyClassVar(sanitizedType);
+ }
+
+ printer.print(sanitizedType);
+ printer.println();
+ }
+
+ if (writeJavaDoc(field, printer, indent, "")) {
+ printer.println();
+ }
+ }
+
+ /**
+ * Wraps the provided type in typing.ClassVar
+ *
+ * @param type the type to wrap
+ * @return the wrapped type
+ */
+ private static String applyClassVar(String type) {
+ if (!type.isEmpty()) {
+ return "typing.ClassVar[" + type + ']';
+ }
+ return type;
+ }
+
+ /**
+ * Wraps the provided type in typing.Final
+ *
+ * @param type the type to wrap
+ * @return the wrapped type
+ */
+ private static String applyFinal(String type) {
+ if (!type.isEmpty()) {
+ return "typing.Final[" + type + ']';
+ }
+ return type;
+ }
+
+ /**
+ * Gets a list of TypeVars for only this type
+ *
+ * @return the list of TypeVars for this type
+ */
+ private List getClassTypeVars() {
+ List extends TypeParameterElement> params = el.getTypeParameters();
+ List res = new ArrayList<>(params.size());
+ for (TypeParameterElement param : params) {
+ res.add(param.getSimpleName().toString());
+ }
+ return res;
+ }
+
+ /**
+ * Gets a list of the Python properties to be created for this type
+ *
+ * @return the list of Python properties
+ */
+ private List getProperties() {
+ if (properties != null) {
+ return properties;
+ }
+ properties = getMethods()
+ .stream()
+ .filter(PythonTypeStubMethod::isProperty)
+ .map(PythonTypeStubMethod::asProperty)
+ .collect(Collectors.groupingBy(PythonTypeStubMethod.PropertyMethod::getName))
+ .values()
+ .stream()
+ .map(this::mergeProperties)
+ .flatMap(Optional::stream)
+ .collect(Collectors.toList());
+ return properties;
+ }
+
+ /**
+ * Merges the provided pairs into one Python property
+ *
+ * @param pairs the property pairs
+ * @return an optional Python property
+ */
+ private Optional mergeProperties(List pairs) {
+ Property res = new Property();
+ if (pairs.size() == 1) {
+ PythonTypeStubMethod.PropertyMethod p = pairs.get(0);
+ if (p.isGetter()) {
+ res.getter = p;
+ }
+ else {
+ res.setter = p;
+ }
+ return Optional.of(res);
+ }
+ PythonTypeStubMethod.PropertyMethod getter = pairs.stream()
+ .filter(PythonTypeStubMethod.PropertyMethod::isGetter)
+ .findFirst()
+ .orElse(null);
+ if (getter != null) {
+ // go through all remaining methods and take the first matching pair
+ // it does not matter if one is a boxed primitive and the other is
+ // unboxed because the JavaProperty will use the primitive type anyway
+ PythonTypeStubMethod.PropertyMethod setter = pairs.stream()
+ .filter(PythonTypeStubMethod.PropertyMethod::isSetter)
+ .filter(getter::isPair)
+ .findFirst()
+ .orElse(null);
+ res.getter = getter;
+ res.setter = setter;
+ return Optional.of(res);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Gets a set of the public method names for this type
+ *
+ * @return the set of public method names
+ */
+ private Set getMethodNames() {
+ if (methodNames != null) {
+ return methodNames;
+ }
+ methodNames = getMethods().stream()
+ .map(PythonTypeStubMethod::getName)
+ .collect(Collectors.toCollection(() -> new HashSet<>(getMethods().size())));
+ return methodNames;
+ }
+
+ /**
+ * Gets a set of the public field names for this type
+ *
+ * @return the set of public field names
+ */
+ private Set getFieldNames() {
+ if (fieldNames != null) {
+ return fieldNames;
+ }
+ fieldNames = getFields().stream()
+ .map(VariableElement::getSimpleName)
+ .map(Object::toString)
+ .map(PythonTypeStubElement::sanitize)
+ .collect(Collectors.toCollection(() -> new HashSet<>(getFields().size())));
+ return fieldNames;
+ }
+
+ /**
+ * Gets an appropriate Python generic base for the provided type
+ *
+ * @param type the generic type
+ * @param params the type parameters
+ * @return the parameterized generic base type
+ */
+ private static String getGenericBase(String type, Iterable params) {
+ String generic = GENERIC_CUSTOMIZERS.getOrDefault(type, "typing.Generic");
+ return generic + "[" + String.join(", ", params) + "]";
+ }
+
+ /**
+ * Prints the first part of the Python class definition
+ *
+ * @param printer the printer
+ * @param indent the indentation
+ */
+ private void printClassDefinition(PrintWriter printer, String indent) {
+ if (!isPublic(el)) {
+ printer.print(indent);
+ printer.println("@typing.type_check_only");
+ }
+ if (doclet.isDeprecated(el)) {
+ String msg = doclet.getDeprecatedMessage(el);
+ if (msg != null) {
+ // a message is required
+ // if one is not present, don't apply it
+ printer.print(indent);
+ printer.print("@deprecated(");
+ printer.print(msg);
+ printer.println(')');
+ }
+ }
+ printer.print(indent);
+ printer.print("class ");
+ printer.print(getName());
+
+ String base = getSuperClass();
+ if (base == null) {
+ // edge case, this is java.lang.Object
+ printer.println(":");
+ indent = indent(indent);
+ writeJavaDoc(printer, indent);
+ printer.println();
+ return;
+ }
+
+ Stream bases;
+ if (el.getInterfaces().isEmpty()) {
+ bases = Stream.of(base);
+ }
+ else if (base.equals(OBJECT_NAME)) {
+ // Object base isn't needed
+ bases = getInterfaces();
+ }
+ else {
+ bases = Stream.concat(Stream.of(base), getInterfaces());
+ }
+
+ List typeParams = getClassTypeVars();
+ if (!typeParams.isEmpty()) {
+ String type = el.getQualifiedName().toString();
+ String genericBase = getGenericBase(type, typeParams);
+ bases = Stream.concat(bases, Stream.of(genericBase));
+ }
+
+ Iterator it = bases.iterator();
+ String baseList = String.join(", ", (Iterable) () -> it);
+ if (!baseList.isEmpty()) {
+ printer.print("(");
+ printer.print(baseList);
+ printer.print(")");
+ }
+ printer.println(":");
+ indent = indent(indent);
+ if (getNestedTypes().isEmpty() && getFields().isEmpty() && getMethods().isEmpty()) {
+ writeJavaDoc(printer, indent, "...");
+ }
+ else {
+ writeJavaDoc(printer, indent);
+ }
+ printer.println();
+ }
+
+ /**
+ * Converts the provided float constant to a Python constant
+ *
+ * @param value the value
+ * @return the Python float constant
+ */
+ private static String convertFloatConstant(double value) {
+ if (Double.isInfinite(value)) {
+ if (value < 0.0f) {
+ return "float(\"-inf\")";
+ }
+ return "float(\"inf\")";
+ }
+ if (Double.isNaN(value)) {
+ return "float(\"nan\")";
+ }
+ return Double.toString(value);
+ }
+
+ /**
+ * Converts the provided field to a Python constant if applicable
+ *
+ * @param field the field
+ * @return the constant value or null
+ */
+ private String getConstantValue(VariableElement field) {
+ Object value = field.getConstantValue();
+ return switch (value) {
+ case String str -> doclet.getStringLiteral(str);
+ case Character str -> doclet.getStringLiteral(str);
+ case Boolean flag -> flag ? "True" : "False";
+ case Float dec -> convertFloatConstant(dec);
+ case Double dec -> convertFloatConstant(dec);
+ case null -> null;
+ default -> value.toString();
+ };
+ }
+
+ /**
+ * Checks if this element is an undocumented override
+ *
+ * @param child the element to check
+ * @return true if this override has no additional documentation
+ */
+ private boolean isUndocumentedOverride(Element child) {
+ if (!doclet.hasJavadoc(child)) {
+ return child.getAnnotation(Override.class) != null;
+ }
+ if (doclet.hasJavadocTag(child, DocTree.Kind.INHERIT_DOC)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if this element is visible
+ *
+ * @param child the element to check
+ * @param protectedScope true to include protected scope
+ * @return true if this element is visible
+ */
+ private boolean isVisible(Element child, boolean protectedScope) {
+ if (isPublic(child)) {
+ return true;
+ }
+ if (protectedScope) {
+ return isProtected(child);
+ }
+ return false;
+ }
+
+ /**
+ * Gets the base class to use for this type
+ *
+ * @return the base class
+ */
+ private String getSuperClass() {
+ TypeMirror base = el.getSuperclass();
+ if (base.getKind() == TypeKind.NONE) {
+ if (el.getQualifiedName().toString().equals(OBJECT_NAME)) {
+ return null;
+ }
+ return OBJECT_NAME;
+ }
+ return sanitizeQualifiedName(el, base);
+ }
+
+ private String sanitizeQualifiedName(TypeMirror type) {
+ return sanitizeQualifiedName(el, type);
+ }
+
+ /**
+ * Gets the interfaces for this type
+ *
+ * @return the interfaces
+ */
+ private Stream getInterfaces() {
+ return el.getInterfaces()
+ .stream()
+ .map(this::sanitizeQualifiedName);
+ }
+
+ /**
+ * Helper for creating a Python property
+ */
+ class Property {
+ PythonTypeStubMethod.PropertyMethod getter;
+ PythonTypeStubMethod.PropertyMethod setter;
+
+ /**
+ * Prints this property to the provided printer
+ *
+ * @param printer the printer
+ * @param indent the indentation
+ */
+ void process(PrintWriter printer, String indent) {
+ if (getter == null) {
+ // only possible at runtime
+ return;
+ }
+ String name = getter.getName();
+ if (name.equals("property")) {
+ // it's not a keyword but it makes the type checker go haywire
+ // just blacklist it
+ return;
+ }
+ if (getMethodNames().contains(name) || getFieldNames().contains(name)) {
+ // do not redefine a method or field
+ return;
+ }
+ String type = sanitizeQualifiedName(getter.getType());
+ printer.print(indent);
+ printer.println("@property");
+ printer.print(indent);
+ printer.print("def ");
+ printer.print(name);
+ printer.print("(self) -> ");
+ printer.print(type);
+ printer.println(":");
+ indent = indent(indent);
+ printer.print(indent);
+ printer.println("...");
+ printer.println();
+
+ if (setter != null) {
+ indent = deindent(indent);
+ printer.print(indent);
+ printer.print("@");
+ printer.print(name);
+ printer.println(".setter");
+ printer.print(indent);
+ printer.print("def ");
+ printer.print(name);
+ printer.print("(self, value: ");
+ printer.print(type);
+ printer.println("):");
+ indent = indent(indent);
+ printer.print(indent);
+ printer.println("...");
+ printer.println();
+ }
+ }
+ }
+}
diff --git a/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/RstTableBuilder.java b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/RstTableBuilder.java
new file mode 100644
index 0000000000..e11895d298
--- /dev/null
+++ b/GhidraBuild/BuildFiles/Doclets/src/main/java/ghidra/doclets/typestubs/RstTableBuilder.java
@@ -0,0 +1,356 @@
+package ghidra.doclets.typestubs;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.lang.model.element.Element;
+
+import com.sun.source.doctree.DocTree;
+
+/**
+ * Helper class for converting an HTML table to reStructuredText
+ */
+final class RstTableBuilder {
+
+ // give each column enough padding to allow an alignment
+ private static final int COLUMN_PADDING = 2;
+
+ private final HtmlConverter docConverter;
+ private final Element el;
+ private Row columns = new Row();
+ private List rows = new ArrayList<>();
+ private Row currentRow = null;
+ private List columnWidths = new ArrayList<>();
+ private String caption = null;
+
+ /**
+ * Creates a new {@link RstTableBuilder}
+ *
+ * @param docConverter the html converter
+ * @param el the element
+ */
+ RstTableBuilder(HtmlConverter docConverter, Element el) {
+ this.docConverter = docConverter;
+ this.el = el;
+ }
+
+ /**
+ * Adds new row group to the table
+ *
+ * @param tree the html tree containing the row group
+ * @throws UnsupportedOperationException if any row in the group contains a nested row
+ */
+ void addRowGroup(HtmlDocTree tree) {
+ switch (tree.getHtmlKind()) {
+ case THEAD:
+ if (tree.getBody().size() > 1) {
+ throw new UnsupportedOperationException("nested table rows are not supported");
+ }
+ case TBODY:
+ case TFOOT:
+ for (DocTree tag : tree.getBody()) {
+ if (!(tag instanceof HtmlDocTree)) {
+ continue;
+ }
+ addRow((HtmlDocTree) tag);
+ }
+ return;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Adds new row to the table
+ *
+ * @param tree the html tree containing the row
+ * @throws UnsupportedOperationException if the row contains a nested row
+ */
+ void addRow(HtmlDocTree tree) {
+ if (currentRow == null) {
+ currentRow = columns;
+ }
+ else {
+ currentRow = new Row();
+ rows.add(currentRow);
+ }
+ boolean columnsDone = columns.size() > 0;
+ for (DocTree tag : tree.getBody()) {
+ if (!(tag instanceof HtmlDocTree)) {
+ continue;
+ }
+ HtmlDocTree html = (HtmlDocTree) tag;
+ String align = docConverter.getAttributes(el, html.getStartTag()).get("align");
+ switch (html.getHtmlKind()) {
+ case TH:
+ if (columnsDone) {
+ // vertical headers
+ // insert it as an entry so it at least comes out ok
+ addEntry(getBody(html), align);
+ }
+ else {
+ addColumn(getBody(html), align);
+ }
+ break;
+ case TD:
+ addEntry(getBody(html), align);
+ break;
+ case TR:
+ throw new UnsupportedOperationException("nested table rows are not supported");
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Adds a caption to the table
+ *
+ * @param caption the caption
+ */
+ void addCaption(String caption) {
+ if (!caption.isBlank()) {
+ this.caption = caption;
+ }
+ }
+
+ /**
+ * Builds the reStructuredText formatted table
+ *
+ * @return the reStructuredText table
+ */
+ String build() {
+ StringBuilder builder = new StringBuilder();
+ builder.append('\n');
+
+ if (caption != null) {
+ int length = caption.length();
+ builder.repeat('^', length)
+ .append('\n');
+ builder.append(caption)
+ .append('\n')
+ .repeat('^', length)
+ .append('\n');
+ }
+
+ buildRowBorder(builder, '-');
+ columns.build(builder);
+ buildRowBorder(builder, '=');
+
+ for (Row row : rows) {
+ row.build(builder);
+ buildRowBorder(builder, '-');
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Adds a column to the table
+ *
+ * @param value the column value
+ * @param align the column alignment
+ */
+ private void addColumn(String value, String align) {
+ if (align == null) {
+ align = "CENTER";
+ }
+ addColumn(value, Alignment.valueOf(align.toUpperCase()));
+ }
+
+ /**
+ * Adds a column to the table
+ *
+ * @param value the column value
+ * @param align the column alignment
+ */
+ private void addColumn(String value, Alignment align) {
+ int column = columns.size();
+ columns.addValue(value, align);
+ growColumn(value, column);
+ }
+
+ /**
+ * Adds an entry to the current row in the table
+ *
+ * @param value the entry value
+ * @param align the entry alignment
+ */
+ private void addEntry(String value, String align) {
+ if (align == null) {
+ align = "LEFT";
+ }
+ addEntry(value, Alignment.valueOf(align.toUpperCase()));
+ }
+
+ /**
+ * Adds an entry to the current row in the table
+ *
+ * @param value the entry value
+ * @param align the entry alignment
+ */
+ private void addEntry(String value, Alignment align) {
+ int column = currentRow.size();
+ currentRow.addValue(value, align);
+ growColumn(value, column);
+ }
+
+ /**
+ * Helper method to get the converted contents of an html tree
+ *
+ * @param tag the html
+ * @return the converted html
+ */
+ private String getBody(HtmlDocTree tag) {
+ return docConverter.convertTree(el, tag.getBody());
+ }
+
+ /**
+ * Creates a row border with the provided character
+ *
+ * @param builder the string builder
+ * @param c the border character
+ */
+ private void buildRowBorder(StringBuilder builder, char c) {
+ builder.append('+');
+ for (int width : columnWidths) {
+ builder.repeat(c, width)
+ .append('+');
+ }
+ builder.append('\n');
+ }
+
+ /**
+ * Computes the max line width for the provided multi-line text
+ *
+ * @param text the text
+ * @return the max line width
+ */
+ private static int getLineWidth(String text) {
+ // value may be mutiple lines
+ return text.lines()
+ .map(String::stripLeading)
+ .mapToInt(String::length)
+ .max()
+ .getAsInt();
+ }
+
+ /**
+ * Grows the provided column appropriately for the newly added value
+ *
+ * @param value the newly added value
+ * @param column the column number
+ */
+ private void growColumn(String value, int column) {
+ int length = !value.isEmpty() ? getLineWidth(value) + COLUMN_PADDING : COLUMN_PADDING;
+ if (column >= columnWidths.size()) {
+ columnWidths.add(length);
+ return;
+ }
+ if (columnWidths.get(column) < length) {
+ columnWidths.set(column, length);
+ }
+ }
+
+ /**
+ * Aligns the single line value according to the column width and alignment
+ *
+ * @param value the value to align
+ * @param columnWidth the column width
+ * @param align the alignment
+ * @return the aligned value
+ */
+ private static String alignSingleLine(String value, int columnWidth, Alignment align) {
+ int length = value.length();
+ return switch (align) {
+ case LEFT -> value + " ".repeat(columnWidth - length);
+ case CENTER -> {
+ int left = (columnWidth - length) / 2;
+ int right = left;
+ if (left + right + length < columnWidth) {
+ right++;
+ }
+ yield " ".repeat(left) + value + " ".repeat(right);
+ }
+ case RIGHT -> " ".repeat(columnWidth - length) + value;
+ };
+ }
+
+ private static enum Alignment {
+ LEFT,
+ CENTER,
+ RIGHT
+ }
+
+ /**
+ * Helper class for modeling a table row
+ */
+ private class Row {
+ int maxLines = 1;
+ List> values = new ArrayList<>();
+ List alignments = new ArrayList<>();
+
+ /**
+ * Adds the value to the row
+ *
+ * @param value the value
+ * @param align the alignment
+ */
+ void addValue(String value, Alignment align) {
+ List lines = value.lines()
+ .map(String::stripLeading)
+ .collect(Collectors.toList());
+ if (lines.size() > maxLines) {
+ maxLines = lines.size();
+ }
+ values.add(lines);
+ alignments.add(align);
+ }
+
+ /**
+ * Gets the size of this row
+ *
+ * @return the row size
+ */
+ int size() {
+ return values.size();
+ }
+
+ /**
+ * Appends this row to the provided string builder
+ *
+ * @param builder the string builder
+ */
+ void build(StringBuilder builder) {
+ for (int i = 0; i < maxLines; i++) {
+ builder.append('|');
+ for (int j = 0; j < values.size(); j++) {
+ List entry = values.get(j);
+ String value;
+ if (i >= entry.size()) {
+ value = " ".repeat(columnWidths.get(j));
+ }
+ else {
+ value = alignSingleLine(j, entry.get(i));
+ }
+ builder.append(value)
+ .append('|');
+ }
+ builder.append('\n');
+ }
+ }
+
+ /**
+ * Aligns the provided single line value according to the column and its alignent
+ *
+ * @param column the column number
+ * @param value the single line value
+ * @return the aligned value
+ */
+ String alignSingleLine(int column, String value) {
+ int columnLength = columnWidths.get(column);
+ return RstTableBuilder.alignSingleLine(value, columnLength, alignments.get(column));
+ }
+ }
+}
diff --git a/GhidraDocs/InstallationGuide.html b/GhidraDocs/InstallationGuide.html
index 7cbfd2e0c4..a558a09dad 100644
--- a/GhidraDocs/InstallationGuide.html
+++ b/GhidraDocs/InstallationGuide.html
@@ -40,6 +40,7 @@ future releases.
Ghidra Server
Headless (Batch) Mode
Single Jar Mode
+ Pyhidra
Extensions
@@ -98,8 +99,10 @@ Ghidra team if you have a specific need.
- Python3 (3.9 to 3.12; for Debugger support)
+ Python3 (3.9 to 3.12)
+ Python 3.7 to 3.12 for Debugger support
+ Python 3.9 to 3.12 for Pyhidra support
This is available from Python.org or most operating system's
app stores or software repositories. For Linux it is recommended that the system's package
repository be used to install a suitable version of Python.
@@ -306,7 +309,7 @@ is complete.
- LICENSE.txt
+ LICENSE
Ghidra license information.
@@ -436,11 +439,36 @@ mode using the command line. For more information, see the
Normally, Ghidra is installed as an entire directory structure that allows modular inclusion or
removal of feature sets and also provides many files that can be extended or configured. However,
there are times when it would be useful to have all or some subset of Ghidra compressed into a
-single jar file at the expense of configuration options. This makes Ghidra easier to run from the
+single jar file at the expense of configuration options. This makes Ghidra easier to run from the
command line for headless operation or to use as a library of reverse engineering capabilities for
another Java application.
A single ghidra.jar file can be created using the
<GhidraInstallDir> /support/buildGhidraJar script.
+
+ Pyhidra Mode
+Ghidra has integrated the the popular Pyhidra extension to enable native CPython 3 support out of
+the box. To enable this support, Ghidra must be launched from a Python environment using special
+launch scripts.
+
+
+ Navigate to <GhidraInstallDir> /support/
+
+
+ Run pyhidraRun.bat (Windows) or pyhidraRun (Linux or macOS)
+ If the pyhidra Python module has not yet been installed, the script will offer to
+ install it for you, along with its dependencies. If you prefer to install it manually, execute:
+
python3 -m pip install --no-index -f <GhidraInstallDir> /Ghidra/Features/Pyhidra/pypkg/dist pyhidra
+ NOTE: You may also install and run Pyhidra from within a
+ virtual environment if you desire.
+ If Ghidra failed to launch, see the Troubleshooting section.
+
+
+
+Once Pyhidra has been installed, you are free to use it like any other Python module. You may
+import it from other Python scripts, or launch Pyhidra using the pyhidra or pyhidraw
+commands. For more information on using Pyhidra, see the
+<GhidraInstallDir> /Ghidra/Features/Pyhidra/Pyhidra_README.html file.
+
(Back to Top )
Extensions
diff --git a/build.gradle b/build.gradle
index e2378d5c16..c0abec5b7d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -111,6 +111,7 @@ apply from: 'gradle/root/distribution.gradle' // adds zip tasks
apply from: 'gradle/root/usage.gradle' // adds task documentation
apply from: "gradle/root/svg.gradle" // adds task to process svg files
apply from: "gradle/root/jacoco.gradle" // adds tasks for java code coverage
+apply from: "gradle/root/venv.gradle" // adds tasks python virtual environments
apply plugin: 'base'
diff --git a/gradle/distributableGhidraModule.gradle b/gradle/distributableGhidraModule.gradle
index b83bf52123..152ada6c65 100644
--- a/gradle/distributableGhidraModule.gradle
+++ b/gradle/distributableGhidraModule.gradle
@@ -183,6 +183,7 @@ plugins.withType(JavaPlugin) {
}
from (p.projectDir.toString() + "/ghidra_scripts") {
exclude 'bin/'
+ exclude '**/__pycache__/**'
into { zipPath + "/ghidra_scripts" }
}
diff --git a/gradle/hasPythonPackage.gradle b/gradle/hasPythonPackage.gradle
index e073e37fc7..7f40010229 100644
--- a/gradle/hasPythonPackage.gradle
+++ b/gradle/hasPythonPackage.gradle
@@ -70,7 +70,9 @@ rootProject.assembleDistribution {
exclude '**/*.pyc'
exclude '**/*.pyo'
exclude '**/__pycache__/**'
- exclude 'dist/*.tar.gz'
+ exclude '**/.pytest_cache'
+ exclude '**/*.egg-info'
+ exclude 'build'
into { zipPath + "/pypkg" }
}
}
diff --git a/gradle/javadoc.gradle b/gradle/javadoc.gradle
index 4788748306..86127ab5fc 100644
--- a/gradle/javadoc.gradle
+++ b/gradle/javadoc.gradle
@@ -28,3 +28,7 @@ rootProject.createJavadocs {
rootProject.createJsondocs {
source sourceSets.main.allJava
}
+
+rootProject.createPythonTypeStubs {
+ source sourceSets.main.allJava
+}
diff --git a/gradle/root/distribution.gradle b/gradle/root/distribution.gradle
index ab7876cc94..6ce7172fef 100644
--- a/gradle/root/distribution.gradle
+++ b/gradle/root/distribution.gradle
@@ -194,6 +194,94 @@ task createJsondocs(type: Javadoc, description: 'Generate JSON docs for all proj
}
}
+task createPythonTypeStubs(type: Javadoc, description: 'Generate pyi stubs for all projects', group: 'Documentation') {
+
+ group 'private'
+
+ String ROOT_PROJECT_DIR = rootProject.projectDir.toString()
+
+ destinationDir file(ROOT_PROJECT_DIR + "/build/typestubs/src")
+
+ failOnError false
+
+ // Must add classpath for main and test source sets. Javadoc will fail if it cannot
+ // find referenced classes.
+ classpath = rootProject.ext.ghidraPath
+
+
+ // Generate at package level because user may try to get help directly on an object they have
+ // rather than its public interface.
+ options.addBooleanOption("package", true)
+
+ // Set the ghidra flag to enable the creation of the ghidra_builtins pseudo package
+ options.addBooleanOption("ghidra", true)
+
+ // Newer versions of gradle set this to true by default.
+ // The JsonDoclet doesn't have the -notimestamp option so ensure it isn't set.
+ options.setNoTimestamp(false)
+
+ // Some internal packages are not public and need to be exported.
+ options.addMultilineStringsOption("-add-exports").setValue(["java.desktop/sun.awt=ALL-UNNAMED"])
+
+ options.doclet = "ghidra.doclets.typestubs.PythonTypeStubDoclet"
+ doFirst {
+ options.docletpath = new ArrayList(configurations.jsondoc.files)
+ }
+}
+
+
+task createGhidraStubsWheel {
+ group 'private'
+ description "Creates the ghidra-stubs wheel for the Ghidra api. [gradle/root/distribution.gradle]"
+
+ dependsOn("createPythonTypeStubs")
+
+ String ROOT_PROJECT_DIR = rootProject.projectDir.toString()
+
+ def cwd = file(ROOT_PROJECT_DIR + "/build/typestubs")
+ def destinationDir = file(cwd.toString() + "/dist")
+
+ it.outputs.file(destinationDir.toString() + "/ghidra_stubs-${project.version}-py3-none-any.whl")
+
+ doFirst {
+ copy {
+ from(file(ROOT_PROJECT_DIR + "/LICENSE"))
+ into cwd
+ }
+
+ def manifest = file(cwd.toString() + "/MANIFEST.in" )
+ manifest.write("graft src\n")
+
+ def pyproject = file(cwd.toString() + "/pyproject.toml" )
+ pyproject.write("""\
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "ghidra-stubs"
+ version = "${project.version}"
+ classifiers = [
+ "License :: OSI Approved :: Apache Software License",
+ "Typing :: Stubs Only",
+ ]
+ """.stripIndent()
+ )
+ }
+
+ doLast {
+ File setuptools = project(":Debugger-rmi-trace").findPyDep(".")
+ exec {
+ workingDir { cwd.toString() }
+ commandLine rootProject.PYTHON3, "-m", "pip"
+ args "wheel", "-w", destinationDir.toString(), "--no-index"
+ args "-f", setuptools
+ args "."
+ }
+ }
+}
+
+
/*********************************************************************************
* JAVADOCS - ZIP
*
@@ -336,6 +424,13 @@ task assembleDistribution (type: Copy) {
from (zipJavadocs) {
into 'docs'
}
+
+ ////////////////////////////
+ // Ghidra Python type stubs
+ ////////////////////////////
+ from (createGhidraStubsWheel) {
+ into 'docs'
+ }
////////////////
// Patch Readme
diff --git a/gradle/root/venv.gradle b/gradle/root/venv.gradle
new file mode 100644
index 0000000000..0ed3d55e52
--- /dev/null
+++ b/gradle/root/venv.gradle
@@ -0,0 +1,17 @@
+
+/******************************************************************************************
+ * TASK createPythonVirtualEnvironment
+ *
+ * Summary: Creates a Python virtual environment directory at "build/venv"
+ ******************************************************************************************/
+task createPythonVirtualEnvironment(type: Exec) {
+ def venvDir = "build/venv"
+ def binDir = isCurrentWindows() ? "Scripts" : "bin"
+ def suffix = isCurrentWindows() ? ".exe" : "3"
+ project.ext.PYTHON3_VENV = "${rootProject.projectDir}/${venvDir}/${binDir}/python${suffix}"
+ project.ext.PIP3_VENV = "${rootProject.projectDir}/${venvDir}/${binDir}/pip${suffix}"
+
+ commandLine rootProject.PYTHON3, "-m", "venv", venvDir, "--copies"
+}
+
+rootProject.prepDev.dependsOn createPythonVirtualEnvironment
\ No newline at end of file
diff --git a/gradle/support/fetchDependencies.gradle b/gradle/support/fetchDependencies.gradle
index f0f7fda81b..69cf7491af 100644
--- a/gradle/support/fetchDependencies.gradle
+++ b/gradle/support/fetchDependencies.gradle
@@ -182,7 +182,7 @@ ext.deps = [
name: "setuptools-68.0.0-py3-none-any.whl",
url: "https://files.pythonhosted.org/packages/c7/42/be1c7bbdd83e1bfb160c94b9cafd8e25efc7400346cf7ccdbdb452c467fa/setuptools-68.0.0-py3-none-any.whl",
sha256: "11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
- destination: file("${DEPS_DIR}/Debugger-rmi-trace/")
+ destination: [file("${DEPS_DIR}/Debugger-rmi-trace/"), file("${DEPS_DIR}/Pyhidra/")]
],
[
name: "wheel-0.37.1-py2.py3-none-any.whl",
@@ -213,6 +213,84 @@ ext.deps = [
url: "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl",
sha256: "37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e",
destination: file("${DEPS_DIR}/Debugger-agent-dbgeng/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ url: "https://files.pythonhosted.org/packages/5d/cf/7b89469bcede4b2fd69c2db7d1d61e8759393cfeec46f7b0c84f5006a691/JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ sha256: "f7aa1469d75f9b310f709b61bb2faa4cef4cbd4d670531ad1d1bb53e29cfda05",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp39-cp39-win_amd64.whl",
+ url: "https://files.pythonhosted.org/packages/b9/fd/d38a8e401b089adce04c48021ddcb366891d1932db2f7653054feb470ae6/JPype1-1.5.0-cp39-cp39-win_amd64.whl",
+ sha256: "6bfdc101c56cab0b6b16e974fd8cbb0b3f7f14178286b8b55413c5d82d5f2bea",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl",
+ url: "https://files.pythonhosted.org/packages/84/9c/80d5edf6d610f82d0658b6402cdf3f8cdd6a7d4f36afb2149da90e0cad47/JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl",
+ sha256: "7b6b1af3f9e0033080e3532c2686a224cd14706f36c14ef36160a2a1db751a17",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ url: "https://files.pythonhosted.org/packages/74/98/d6517002355b0585d0e66f7b0283c7f6e2271c898a886e1ebac09836b100/JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ sha256: "a02b2f05621c119d35f4acc501b4261eeb48a4af7cc13d9afc2e9eb316c4bd29",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp310-cp310-win_amd64.whl",
+ url: "https://files.pythonhosted.org/packages/da/5f/253c1c1dba6f7f457b6c3aa2ea9c517287d49764e0ee1042d5818c36e781/JPype1-1.5.0-cp310-cp310-win_amd64.whl",
+ sha256: "0b40c76e075d4fed2c83340bb30b7b95bbc396fd370c564c6b608faab00ea4ef",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl",
+ url: "https://files.pythonhosted.org/packages/98/37/0049866cbfecb879b46d8e9f9b70944624ab17152a282ad5cf60909054ec/JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl",
+ sha256: "85a31b30b482eaf788b21af421e0750aa0be7758307314178143a76632b0ad04",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ url: "https://files.pythonhosted.org/packages/17/1e/7728ae8fb41e8fbf3a7309f8936d07b0b1622f2860733df0e7ec30b1ce76/JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ sha256: "5ef976e0f3b2e9604469f449f30bb2031941a159a0637f4c16adb2c5076f3e81",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp311-cp311-win_amd64.whl",
+ url: "https://files.pythonhosted.org/packages/1f/19/144f3a767b563ba5c6d4aa534ea1f3fad9a5067c3917df4458a6e1afe0ef/JPype1-1.5.0-cp311-cp311-win_amd64.whl",
+ sha256: "2bc987205ff8d2d8e36dfbef05430e0638e85d4fee1166ba58ebfa6f7a67cdf8",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl",
+ url: "https://files.pythonhosted.org/packages/30/0d/9ac6f0e59427fc5ebf4547c2fdbb38e347b46c2dc20b430490236d037ed8/JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl",
+ sha256: "8714bfaf09d6877160bc7ac97812016ccb09f6d7ba5ea2a9f519178aefcca93f",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ url: "https://files.pythonhosted.org/packages/7d/ed/549766039d17550da6e3fa59ed776a021b400324d7766358d3b6e33d8b28/JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+ sha256: "8649b526eccb4047881ad60bdb1974eb71a09cdb7f8bda17c96fdc0f9a3f2d1e",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0-cp312-cp312-win_amd64.whl",
+ url: "https://files.pythonhosted.org/packages/20/47/9606af72e21703e5fca5e29e5bd5e345506977b6ba492c549648adef47ef/JPype1-1.5.0-cp312-cp312-win_amd64.whl",
+ sha256: "9aafc00b00bf8c1b624081e5d4ab87f7752e6c7ee6a141cfc332250b05c6d42f",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "JPype1-1.5.0.tar.gz",
+ url: "https://files.pythonhosted.org/packages/25/42/8ca50a0e27e3053829545829e7bcba071cbfa4d5d8fd7fc5d1d988f325b1/JPype1-1.5.0.tar.gz",
+ sha256: "425a6e1966afdd5848b60c2688bcaeb7e40ba504a686f1114589668e0631e878",
+ destination: file("${DEPS_DIR}/Pyhidra/")
+ ],
+ [
+ name: "packaging-23.2-py3-none-any.whl",
+ url: "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl",
+ sha256: "8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7",
+ destination: file("${DEPS_DIR}/Pyhidra/")
]
]
@@ -231,12 +309,20 @@ deps.each {
// Copies the downloaded dependencies to their required destination.
// Some downloads require pre-processing before their relevant pieces can be copied.
deps.each {
- if (it.destination instanceof File) {
+ def copier = { File fp ->
if (!OFFLINE) {
- println "Copying " + it.name + " to " + it.destination
+ println "Copying " + it.name + " to " + fp
+ }
+ mkdirs(fp)
+ copyFile(new File(DOWNLOADS_DIR, it.name), new File(fp, it.name));
+ }
+ if (it.destination instanceof File) {
+ copier(it.destination)
+ }
+ else if (it.destination instanceof List) {
+ it.destination.each { fp ->
+ copier(fp)
}
- mkdirs(it.destination)
- copyFile(new File(DOWNLOADS_DIR, it.name), new File(it.destination, it.name));
}
else if (it.destination instanceof Closure) {
if (!OFFLINE) {
@@ -267,7 +353,7 @@ def download(url, file) {
println "curl -L -o " + relative(file) + " '" + url + "'"
return
}
-
+
println "URL: " + url
def(InputStream istream, size) = establishConnection(url, NUM_RETRIES);
assert istream != null : " ***CONNECTION FAILURE***\n max attempts exceeded; exiting\n"
@@ -341,7 +427,7 @@ def unzip(sourceDir, targetDir, zipFileName) {
println "unzip " + relative(zipFile) + " -d " + relative(targetDir)
return
}
-
+
def zip = new ZipFile(zipFile)
zip.entries().findAll { !it.directory }.each { e ->
(e.name as File).with { f ->
@@ -386,7 +472,7 @@ def copyFile(sourceFile, targetFile) {
println "cp " + relative(sourceFile) + " " + relative(targetFile)
return
}
-
+
FileUtils.copyFile(sourceFile, targetFile)
}