Python3 support

This commit is contained in:
DC3-TSD 2024-09-09 09:56:46 -04:00
parent d7c1f65f43
commit 92d0f1dacf
101 changed files with 11413 additions and 13 deletions

5
.gitignore vendored
View File

@ -86,3 +86,8 @@ Release
*.log *.log
core.* core.*
!core.png !core.png
!core.py
# python files
*.egg-info
__pycache__

View File

@ -65,6 +65,7 @@ public class VSCodeProjectScript extends GhidraScript {
writeSettings(installDir, projectDir, classpathSourceMap); writeSettings(installDir, projectDir, classpathSourceMap);
writeLaunch(installDir, projectDir, classpathSourceMap); writeLaunch(installDir, projectDir, classpathSourceMap);
writeSampleScriptJava(projectDir); writeSampleScriptJava(projectDir);
writeSampleScriptPyhidra(projectDir);
writeSampleModule(installDir, projectDir); writeSampleModule(installDir, projectDir);
println("Successfully created VSCode project directory at: " + projectDir); println("Successfully created VSCode project directory at: " + projectDir);
@ -227,6 +228,25 @@ public class VSCodeProjectScript extends GhidraScript {
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8); 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 * Write a sample Java-based Ghidra module into the VSCode project directory
* *

1
Ghidra/Features/Pyhidra/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.pytest_cache/

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.remoteJavaApplication">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<booleanAttribute key="org.eclipse.jdt.launching.ALLOW_TERMINATE" value="true"/>
<mapAttribute key="org.eclipse.jdt.launching.CONNECT_MAP">
<mapEntry key="hostname" value="localhost"/>
<mapEntry key="port" value="18001"/>
</mapAttribute>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Features Pyhidra"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_CONNECTOR_ID" value="org.eclipse.jdt.launching.socketAttachConnector"/>
</launchConfiguration>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.debug.core.groups.GroupLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.action" value="OUTPUT_REGEXP"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.actionParam" value="Listening for transport dt_socket at address: \d+"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.adoptIfRunning" value="false"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.name" value="_Pyhidra GUI Debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.action" value="NONE"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.adoptIfRunning" value="true"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.name" value="Ghidra Attach"/>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
</listAttribute>
</launchConfiguration>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
</mapAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
</listAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v&#13;&#10;-g"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.debug.core.groups.GroupLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.action" value="OUTPUT_REGEXP"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.actionParam" value="Listening for transport dt_socket at address: \d+"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.adoptIfRunning" value="false"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.name" value="_Pyhidra Interpreter Debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.action" value="NONE"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.adoptIfRunning" value="true"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.name" value="Ghidra Attach"/>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
</listAttribute>
</launchConfiguration>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
</mapAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
</listAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
<mapEntry key="PYHIDRA_DEBUG" value="1"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v&#13;&#10;-g"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
<mapEntry key="PYHIDRA_DEBUG" value="1"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>

View File

@ -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

View File

@ -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 }
}
}

View File

@ -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|

View File

@ -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]

View File

@ -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 <java class 'byte[]'>
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

View File

@ -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='<install dir>', 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()

View File

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<tocroot>
<tocref id="Ghidra Functionality">
<tocref id="Scripting">
<tocdef id="Pyhidra Interpreter" sortgroup="z" text="Pyhidra Interpreter" target="help/topics/Pyhidra/interpreter.html" />
</tocref>
</tocref>
</tocroot>

View File

@ -0,0 +1,167 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>Pyhidra Interpreter</TITLE>
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Pyhidra"></A>Pyhidra Interpreter</H1>
<P>
The Ghidra <I>Pyhidra Interpreter</I> 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.
</P>
<H2>Environment</H2>
<BLOCKQUOTE>
<P>
The Ghidra <I>Pyhidra Interpreter</I> is configured to run in a similar context as a Ghidra
script. Therefore, you immediately have access to variables such as <TT>currentProgram</TT>,
<TT>currentSelection</TT>, <TT>currentAddress</TT>, 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.
</P>
<P>
As in Java, classes outside of your current package/module need to be explicitly imported.
For example, consider the following code snippet:
</P>
<BR>
<PRE>
<FONT COLOR="GREEN"># Get a data type from the user</FONT>
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))
</PRE>
<P>
<TT>currentProgram</TT> and <TT>state</TT> are defined within the Ghidra scripting class
hierarchy, so nothing has to be explicitly imported before they can be used. However, because
the <TT>DataTypeSelectionDialog</TT> class and <TT>AllowedDataType</TT> enum reside in
different packages, they must be explicitly imported. Failure to do so will result in a
Python <TT><FONT COLOR="RED">NameError</FONT></TT>.
</P>
</BLOCKQUOTE>
<H2><A name="Clear_Interpreter"></A>Clear <IMG border="0" src="images/erase16.png"></H2>
<BLOCKQUOTE>
<P>
This command clears the interpreter's display. Its effect is purely visual.
It does not affect the state of the interpreter in any way.
</P>
</BLOCKQUOTE>
<H2><A name="Interrupt_Interpreter"></A>Interrupt <IMG border="0" src="images/dialog-cancel.png"></H2>
<BLOCKQUOTE>
<P>
This command issues a keyboard interrupt to the interpreter, which can be used to interrupt
long running commands or loops.
</P>
</BLOCKQUOTE>
<H2><A name="Reset_Interpreter"></A>Reset <IMG border="0" src="images/reload3.png"></H2>
<BLOCKQUOTE>
<P>
This command resets the interpreter, which clears the display and resets all state.
</P>
</BLOCKQUOTE>
<H2>Keybindings</H2>
<BLOCKQUOTE>
<P>
The Ghidra <I>Pyhidra Interpreter</I> supports the following hard-coded keybindings:
<UL>
<LI><B>(up):</B>&nbsp;&nbsp;Move backward in command stack</LI>
<LI><B>(down):</B>&nbsp;&nbsp;Move forward in command stack</LI>
<LI><B>TAB:</B>&nbsp;&nbsp;Show code completion window</LI>
</UL>
<P>
With the code completion window open:
<UL>
<LI><B>TAB:</B>&nbsp;&nbsp;Insert currently-selected code completion (if no completion selected, select the first available)</LI>
<LI><B>ENTER:</B>&nbsp;&nbsp;Insert selected completion (if any) and close the completion window</LI>
<LI><B>(up):</B>&nbsp;&nbsp;Select previous code completion</LI>
<LI><B>(down):</B>&nbsp;&nbsp;Select next code completion</LI>
<LI><B>ESC:</B>&nbsp;&nbsp;Hide code completion window</LI>
</UL>
</P>
</BLOCKQUOTE>
<H2>Copy/Paste</H2>
<BLOCKQUOTE>
<P>
Copy and paste from within the Ghidra <I>Pyhidra Interpreter</I> should work as expected for
your given environment:
<UL>
<LI><B>Windows:</B>&nbsp;&nbsp;CTRL+C / CTRL+V</LI>
<LI><B>Linux:</B>&nbsp;&nbsp;CTRL+C / CTRL+V</LI>
<LI><B>OS X:</B>&nbsp;&nbsp;COMMAND+C / COMMAND+V</LI>
</UL>
</P>
</BLOCKQUOTE>
<H2>API Documentation</H2>
<BLOCKQUOTE>
<P>
The built-in <TT>help()</TT> Python function has been altered by the Ghidra <I>Pyhidra Interpreter</I>
to add support for displaying Ghidra's Javadoc (where available) for a given Ghidra class, method,
or variable. For example, to see Ghidra's Javadoc on the <TT>state</TT> variable, simply do:
<PRE>
>>> help(state)
#####################################################
class ghidra.app.script.GhidraState
extends java.lang.Object
Represents the current state of a Ghidra tool
#####################################################
PluginTool getTool()
Returns the current tool.
@return ghidra.framework.plugintool.PluginTool: the current tool
-----------------------------------------------------
Project getProject()
Returns the current project.
@return ghidra.framework.model.Project: the current project
-----------------------------------------------------
...
...
...
</PRE>
<P>
Calling help() with no arguments will show the Javadoc for the GhidraScript class.
</P>
</BLOCKQUOTE>
<H2>Additional Help</H2>
<BLOCKQUOTE>
<P>
For more information on the Jpype environment, such as how to interact with Java objects
through a Python interface, please refer to Jpype's documentation which can be found on the
Internet at <I><B>jpype.readthedocs.io</B></I>
</P>
</BLOCKQUOTE>
<P align="left" class="providedbyplugin">Provided by: <I>PyhidraPlugin</I></P>
<P>&nbsp;</P>
<BR>
<BR>
<BR>
</BODY>
</HTML>

View File

@ -0,0 +1,104 @@
package ghidra.pyhidra;
import java.util.function.Consumer;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.app.plugin.core.interpreter.*;
import ghidra.app.script.GhidraState;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.pyhidra.interpreter.InterpreterGhidraScript;
import ghidra.pyhidra.interpreter.PyhidraInterpreter;
import ghidra.util.exception.AssertException;
/**
* This plugin provides the interactive Python interpreter.
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.COMMON,
shortDescription = "Pyhidra Interpreter",
description = "Provides an interactive Python Interpreter that is tightly integrated with a loaded Ghidra program.",
servicesRequired = { InterpreterPanelService.class }
)
//@formatter:on
public class PyhidraPlugin extends ProgramPlugin {
public static final String TITLE = "Pyhidra";
private static Consumer<PyhidraPlugin> 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.<p>
*
* This method is for <b>internal use only</b> 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<PyhidraPlugin> 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);
}
}

View File

@ -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<GhidraScript> scriptRunner = null;
/**
* Sets the Python side script runner.
*
* This method is for <b>internal use only</b> 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<GhidraScript> 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);
}
}
}
}

View File

@ -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 <b>internal use only</b> 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 <b>internal use only</b> 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<String, ExposedField> 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<String, ExposedField> 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<String, ExposedField>[] 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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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("<pyhidra-interactive>: " + message);
}
}

View File

@ -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 <b>internal use only</b> 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<CodeCompletion> getCompletions(String cmd, int caretPos);
/**
* Restarts the pyhidra console
*/
void restart();
/**
* Interrupts the code running in the pyhidra console
*/
void interrupt();
}

View File

@ -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<CodeCompletion> getCompletions(String cmd) {
throw new AssertException("Unreachable, unimplemented and deprecated method");
}
@Override
public List<CodeCompletion> 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 <b>internal use only</b> 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));
}
}

View File

@ -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();
}
}

View File

@ -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<T> implements JavaProperty<T> 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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>boolean</b> type
*/
public final class BooleanJavaProperty extends AbstractJavaProperty<Boolean> {
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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>byte</b> type
*/
public final class ByteJavaProperty extends AbstractJavaProperty<Byte> {
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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>char</b> type
*/
public final class CharacterJavaProperty extends AbstractJavaProperty<Character> {
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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>double</b> type
*/
public final class DoubleJavaProperty extends AbstractJavaProperty<Double> {
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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>float</b> type
*/
public final class FloatJavaProperty extends AbstractJavaProperty<Float> {
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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>int</b> type
*/
public final class IntegerJavaProperty extends AbstractJavaProperty<Integer> {
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);
}
}

View File

@ -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<T> 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;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>long</b> type
*/
public final class LongJavaProperty extends AbstractJavaProperty<Long> {
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);
}
}

View File

@ -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<Object> {
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);
}
}

View File

@ -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 <b>internal use only</b> 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 <b>internal use only</b> 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<Method> 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<JavaProperty<?>> merge(List<PartialProperty> 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;
}
}
}

View File

@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>short</b> type
*/
public final class ShortJavaProperty extends AbstractJavaProperty<Short> {
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);
}
}

View File

@ -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.

View File

@ -0,0 +1,2 @@
graft tests
global-exclude *.pyc

View File

@ -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 <CLI ARGS PASSED TO SCRIPT>
```
### 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
```

View File

@ -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"]

View File

@ -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()

View File

@ -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"
]

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,3 @@
"""
Internal use only
"""

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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())

View File

@ -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 {
}

View File

@ -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

View File

@ -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 "";
}
}

View File

@ -0,0 +1,4 @@
from example_script import import_test_function
if __name__ == '__main__':
import_test_function()

View File

@ -0,0 +1,5 @@
if __name__ == "__main__":
assert currentProgram is None
assert state.getProject() is not None
print("programless_script executed successfully")

View File

@ -0,0 +1,5 @@
if __name__ == "__main__":
assert currentProgram is None
assert state.getProject() is None
print("projectless_script executed successfully")

View File

@ -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"]

View File

@ -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

View File

@ -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()

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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<String, ExposedField> fields = PythonFieldExposer.getProperties(PyhidraScriptProvider.PyhidraGhidraScript.class);
assertTrue(fields.containsKey("currentProgram"));
}
}

View File

@ -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<Object[]> 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<Object[]> convertData(Class<?>[] classes) {
List<Object[]> 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;
}
}
}

View File

@ -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} "$@"

View File

@ -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%

View File

@ -28,4 +28,5 @@ dependencies {
} }
rootProject.createJsondocs.dependsOn jar rootProject.createJsondocs.dependsOn jar
rootProject.createPythonTypeStubs.dependsOn jar

View File

@ -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<String, String> 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();
}
}

View File

@ -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<String> 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<String> 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<String> exports) {
// methods must be sorted by name for typing.overload
List<PythonTypeStubMethod> apiMethods = api.getMethods(true, true);
List<PythonTypeStubMethod> scriptMethods = script.getMethods(true, true);
int length = apiMethods.size() + scriptMethods.size();
List<PythonTypeStubMethod> methods = new ArrayList<>(length);
methods.addAll(apiMethods);
methods.addAll(scriptMethods);
methods.sort(null);
ListIterator<PythonTypeStubMethod> 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<PackageElement> getScriptPackages() {
Set<PackageElement> packages = new HashSet<>();
for (TypeElement type : api.getImportedTypes()) {
packages.add(PythonTypeStubElement.getPackage(type));
}
for (TypeElement type : script.getImportedTypes()) {
packages.add(PythonTypeStubElement.getPackage(type));
}
List<PackageElement> 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<String> getScriptTypeVars() {
// all this for only two typing.TypeVar
// at least this is future proof
Set<String> vars = new HashSet<>(api.getTypeVars());
vars.addAll(script.getTypeVars());
List<String> 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);
}
}

View File

@ -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<String, String> 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, "<caption> outside of table");
yield start.toString();
}
case CITE -> "*" + convertTree(el, tag.getBody()) + "*";
case CODE -> "``" + convertTree(el, tag.getBody()) + "``";
case DD -> {
logError(el, start, "<dd> 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, "<dt> 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, "<li> 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, "<tbody> outside of table");
yield start.toString();
}
case TD -> {
logError(el, start, "<td> outside of table");
yield start.toString();
}
case TFOOT -> {
logError(el, start, "<tfoot> outside of table");
yield start.toString();
}
case TH -> {
logError(el, start, "<th> outside of table");
yield start.toString();
}
case THEAD -> {
logError(el, start, "<thead> outside of table");
yield start.toString();
}
case TR -> {
logError(el, start, "<tr> 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 <blockquote>} 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 <H1>} ... {@literal <H6>} 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 <li>} 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 <dl>}
*
* @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 <ol>}
*
* @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 <ul>}
*
* @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 <a id="#example">link text</a>}
*
* @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<String, String> 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 <table>} 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 <table>}
*
* @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();
}
}

View File

@ -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<p/>
*
* 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<DocTree> 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<DocTree> 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, D> R accept(DocTreeVisitor<R, D> 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<p/>
*
* 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;
}
}

View File

@ -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<String, HtmlTagKind> 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.<p/>
*
* 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 <p> is NOT a void tag
// unfortunately it is misused so often that the errors/warnings
// would become junk because the <p> 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;
};
}
}

View File

@ -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<String, String> 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<String> JDK_TAGLETS = new HashSet<>(
Set.of("jls", "jvms", "extLink", "Incubating", "moduleGraph", "sealedGraph", "toolGuide"));
private static final Map<String, String> 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<SeeTree> 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<String> 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<String, String> 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<String, String> 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 &amp;lt;})
*
* @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';
}
}

View File

@ -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.<p/>
*
* To run: gradle createPythonTypeStubs
*/
public class PythonTypeStubDoclet implements Doclet {
private Reporter log;
private File destDir;
private DocletEnvironment docEnv;
private JavadocConverter docConverter;
private Set<String> processedPackages;
private Set<String> 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<String> getNames() {
return Arrays.asList("-d");
}
@Override
public String getParameters() {
return "directory";
}
@Override
public boolean process(String option, List<String> 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<String> getNames() {
return Arrays.asList("-ghidra");
}
@Override
public String getParameters() {
return "";
}
@Override
public boolean process(String option, List<String> 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<String> getNames() {
return Arrays.asList("-properties");
}
@Override
public String getParameters() {
return "";
}
@Override
public boolean process(String option, List<String> 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<ModuleElement> 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<PackageElement> 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<p/>
*
* 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<PackageElement> 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<p/>
*
* 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<p/>
*
* 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<p/>
*
* 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();
}
}

View File

@ -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<T extends Element> {
private static final Set<String> 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<p/>
*
* 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<String> it = Arrays.stream(name.split("\\."))
.map(PythonTypeStubElement::sanitize)
.iterator();
return String.join(".", (Iterable<String>) () -> 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<p/>
*
* 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<String> 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);
});
}
}

View File

@ -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<ExecutableElement>
implements Comparable<PythonTypeStubMethod> {
private static final String EMPTY_DOCS = "..." + System.lineSeparator();
private static final Map<String, String> 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<String, String> 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<String> typevars;
Set<TypeElement> 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<Modifier> modifiers = el.getModifiers();
boolean isStatic = modifiers.contains(Modifier.STATIC);
if (name.equals("<init>")) {
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<String> 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<TypeElement> 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<String> 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<String> getParameterNames() {
List<? extends VariableElement> params = el.getParameters();
List<String> 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.<p/>
*
* 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());
}
}

View File

@ -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);
}
}

View File

@ -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<p/>
*
* This will process all visible classes, interfaces, handle necessary imports
* and create the __init__.pyi file.
*/
final class PythonTypeStubPackage extends PythonTypeStubElement<PackageElement> {
private String packageName;
private File path;
private List<PythonTypeStubType> 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<String> getTypeVars() {
Set<String> typevars = new HashSet<>();
for (PythonTypeStubType type : getTypes()) {
typevars.addAll(type.getTypeVars());
}
List<String> 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<TypeElement> getImportedTypes() {
Set<TypeElement> 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<PythonTypeStubType> 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<String> 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<PackageElement> getImportedPackages() {
Set<PackageElement> 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<PackageElement> 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<String> allTypeVars = getTypeVars();
for (String generic : allTypeVars) {
printer.println(generic + " = typing.TypeVar(\"" + generic + "\")");
}
if (!allTypeVars.isEmpty()) {
printer.println();
printer.println();
}
}
}

View File

@ -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<TypeElement> {
private static final String OBJECT_NAME = Object.class.getName();
private static final Map<String, String> 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<TypeElement> imports;
private Set<String> typevars;
private List<PythonTypeStubNestedType> nestedTypes;
private List<VariableElement> fields;
private List<PythonTypeStubMethod> methods;
private List<Property> properties;
private Set<String> fieldNames;
private Set<String> 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<String> 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<TypeElement> 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<PythonTypeStubNestedType> 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<VariableElement> 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<VariableElement> 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<PythonTypeStubMethod> 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<PythonTypeStubMethod> 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<PythonTypeStubMethod> methods,
ListIterator<PythonTypeStubMethod> 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<PythonTypeStubMethod> 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<String> getClassTypeVars() {
List<? extends TypeParameterElement> params = el.getTypeParameters();
List<String> 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<Property> 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<Property> mergeProperties(List<PythonTypeStubMethod.PropertyMethod> 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<String> 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<String> 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<String> 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<String> 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<String> typeParams = getClassTypeVars();
if (!typeParams.isEmpty()) {
String type = el.getQualifiedName().toString();
String genericBase = getGenericBase(type, typeParams);
bases = Stream.concat(bases, Stream.of(genericBase));
}
Iterator<String> it = bases.iterator();
String baseList = String.join(", ", (Iterable<String>) () -> 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<String> 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();
}
}
}
}

View File

@ -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<Row> rows = new ArrayList<>();
private Row currentRow = null;
private List<Integer> 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<List<String>> values = new ArrayList<>();
List<Alignment> alignments = new ArrayList<>();
/**
* Adds the value to the row
*
* @param value the value
* @param align the alignment
*/
void addValue(String value, Alignment align) {
List<String> 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<String> 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));
}
}
}

View File

@ -40,6 +40,7 @@ future releases.
<li><a href="#RunServer">Ghidra Server</a></li> <li><a href="#RunServer">Ghidra Server</a></li>
<li><a href="#RunHeadless">Headless (Batch) Mode</a></li> <li><a href="#RunHeadless">Headless (Batch) Mode</a></li>
<li><a href="#RunJar">Single Jar Mode</a></li> <li><a href="#RunJar">Single Jar Mode</a></li>
<li><a href="#RunPyhidra">Pyhidra</a></li>
</ul> </ul>
<li><a href="#Extensions">Extensions</a></li> <li><a href="#Extensions">Extensions</a></li>
<ul> <ul>
@ -98,8 +99,10 @@ Ghidra team if you have a specific need.</p></blockquote>
</li> </li>
</ul> </ul>
</ul> </ul>
<li>Python3 (3.9 to 3.12; for Debugger support)</li> <li>Python3 (3.9 to 3.12)</li>
<ul> <ul>
<li>Python 3.7 to 3.12 for <a href="#DebuggerPython">Debugger support</a></li>
<li>Python 3.9 to 3.12 for <a href="#RunPyhidra">Pyhidra support</a></li>
<li>This is available from <a href="https://python.org">Python.org</a> or most operating system's <li>This is available from <a href="https://python.org">Python.org</a> or most operating system's
app stores or software repositories. For Linux it is recommended that the system's package 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.</li> repository be used to install a suitable version of Python.</li>
@ -306,7 +309,7 @@ is complete.</p>
</td> </td>
</tr> </tr>
<tr> <tr>
<td valign="top"><b>LICENSE.txt</b></td> <td valign="top"><b>LICENSE</b></td>
<td valign="top">Ghidra license information.</td> <td valign="top">Ghidra license information.</td>
</tr> </tr>
<tr> <tr>
@ -436,11 +439,36 @@ mode using the command line. For more information, see the
<p>Normally, Ghidra is installed as an entire directory structure that allows modular inclusion or <p>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, 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 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 command line for headless operation or to use as a library of reverse engineering capabilities for
another Java application.</p> another Java application.</p>
<p>A single ghidra.jar file can be created using the <p>A single ghidra.jar file can be created using the
<i>&lt;GhidraInstallDir&gt;</i>/support/buildGhidraJar script.</p> <i>&lt;GhidraInstallDir&gt;</i>/support/buildGhidraJar script.</p>
<h3><a name="RunPyhidra"></a>Pyhidra Mode</h3>
<p>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.</p>
<ol>
<li>
Navigate to <i>&lt;GhidraInstallDir&gt;</i>/support/
</li>
<li>
<p>Run <i>pyhidraRun.bat</i> (Windows) or <i>pyhidraRun</i> (Linux or macOS)</p>
<p>If the <b>pyhidra</b> 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:
<pre>python3 -m pip install --no-index -f <i>&lt;GhidraInstallDir&gt;</i>/Ghidra/Features/Pyhidra/pypkg/dist pyhidra</pre>
<b>NOTE: </b>You may also install and run Pyhidra from within a
<a href="https://docs.python.org/3/tutorial/venv.html">virtual environment</a> if you desire.
<p>If Ghidra failed to launch, see the <a href="#Troubleshooting">Troubleshooting</a> section.
</p>
</li>
</ol>
<p>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 <i>pyhidra</i> or <i>pyhidraw</i>
commands. For more information on using Pyhidra, see the
<i>&lt;GhidraInstallDir&gt;</i>/Ghidra/Features/Pyhidra/Pyhidra_README.html file.</p>
<p>(<a href="#top">Back to Top</a>)</p> <p>(<a href="#top">Back to Top</a>)</p>
<h2><a name="Extensions"></a>Extensions</h2> <h2><a name="Extensions"></a>Extensions</h2>

View File

@ -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/usage.gradle' // adds task documentation
apply from: "gradle/root/svg.gradle" // adds task to process svg files 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/jacoco.gradle" // adds tasks for java code coverage
apply from: "gradle/root/venv.gradle" // adds tasks python virtual environments
apply plugin: 'base' apply plugin: 'base'

View File

@ -183,6 +183,7 @@ plugins.withType(JavaPlugin) {
} }
from (p.projectDir.toString() + "/ghidra_scripts") { from (p.projectDir.toString() + "/ghidra_scripts") {
exclude 'bin/' exclude 'bin/'
exclude '**/__pycache__/**'
into { zipPath + "/ghidra_scripts" } into { zipPath + "/ghidra_scripts" }
} }

View File

@ -70,7 +70,9 @@ rootProject.assembleDistribution {
exclude '**/*.pyc' exclude '**/*.pyc'
exclude '**/*.pyo' exclude '**/*.pyo'
exclude '**/__pycache__/**' exclude '**/__pycache__/**'
exclude 'dist/*.tar.gz' exclude '**/.pytest_cache'
exclude '**/*.egg-info'
exclude 'build'
into { zipPath + "/pypkg" } into { zipPath + "/pypkg" }
} }
} }

View File

@ -28,3 +28,7 @@ rootProject.createJavadocs {
rootProject.createJsondocs { rootProject.createJsondocs {
source sourceSets.main.allJava source sourceSets.main.allJava
} }
rootProject.createPythonTypeStubs {
source sourceSets.main.allJava
}

View File

@ -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 * JAVADOCS - ZIP
* *
@ -337,6 +425,13 @@ task assembleDistribution (type: Copy) {
into 'docs' into 'docs'
} }
////////////////////////////
// Ghidra Python type stubs
////////////////////////////
from (createGhidraStubsWheel) {
into 'docs'
}
//////////////// ////////////////
// Patch Readme // Patch Readme
//////////////// ////////////////

17
gradle/root/venv.gradle Normal file
View File

@ -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

View File

@ -182,7 +182,7 @@ ext.deps = [
name: "setuptools-68.0.0-py3-none-any.whl", 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", url: "https://files.pythonhosted.org/packages/c7/42/be1c7bbdd83e1bfb160c94b9cafd8e25efc7400346cf7ccdbdb452c467fa/setuptools-68.0.0-py3-none-any.whl",
sha256: "11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", 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", 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", url: "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl",
sha256: "37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", sha256: "37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e",
destination: file("${DEPS_DIR}/Debugger-agent-dbgeng/") 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. // Copies the downloaded dependencies to their required destination.
// Some downloads require pre-processing before their relevant pieces can be copied. // Some downloads require pre-processing before their relevant pieces can be copied.
deps.each { deps.each {
if (it.destination instanceof File) { def copier = { File fp ->
if (!OFFLINE) { 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<File>) {
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) { else if (it.destination instanceof Closure) {
if (!OFFLINE) { if (!OFFLINE) {