mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-23 04:32:12 +00:00
Python3 support
This commit is contained in:
parent
d7c1f65f43
commit
92d0f1dacf
5
.gitignore
vendored
5
.gitignore
vendored
@ -86,3 +86,8 @@ Release
|
||||
*.log
|
||||
core.*
|
||||
!core.png
|
||||
!core.py
|
||||
|
||||
# python files
|
||||
*.egg-info
|
||||
__pycache__
|
||||
|
@ -65,6 +65,7 @@ public class VSCodeProjectScript extends GhidraScript {
|
||||
writeSettings(installDir, projectDir, classpathSourceMap);
|
||||
writeLaunch(installDir, projectDir, classpathSourceMap);
|
||||
writeSampleScriptJava(projectDir);
|
||||
writeSampleScriptPyhidra(projectDir);
|
||||
writeSampleModule(installDir, projectDir);
|
||||
|
||||
println("Successfully created VSCode project directory at: " + projectDir);
|
||||
@ -226,6 +227,25 @@ public class VSCodeProjectScript extends GhidraScript {
|
||||
}
|
||||
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void writeSampleScriptPyhidra(File projectDir) throws IOException {
|
||||
File scriptsDir = new File(projectDir, "ghidra_scripts");
|
||||
File scriptFile = new File(scriptsDir, "sample_script.py");
|
||||
String sampleScript = """
|
||||
# Sample Pyhidra GhidraScript
|
||||
# @category Examples
|
||||
# @runtime Pyhidra
|
||||
|
||||
from java.util import LinkedList
|
||||
java_list = LinkedList([1,2,3])
|
||||
|
||||
block = currentProgram.memory.getBlock('.text')
|
||||
""";
|
||||
if (!FileUtilities.mkdirs(scriptFile.getParentFile())) {
|
||||
throw new IOException("Failed to create: " + scriptFile.getParentFile());
|
||||
}
|
||||
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a sample Java-based Ghidra module into the VSCode project directory
|
||||
|
1
Ghidra/Features/Pyhidra/.gitignore
vendored
Normal file
1
Ghidra/Features/Pyhidra/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/.pytest_cache/
|
17
Ghidra/Features/Pyhidra/.launch/Ghidra Attach.launch
Normal file
17
Ghidra/Features/Pyhidra/.launch/Ghidra Attach.launch
Normal 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>
|
17
Ghidra/Features/Pyhidra/.launch/Pyhidra GUI Debug.launch
Normal file
17
Ghidra/Features/Pyhidra/.launch/Pyhidra GUI Debug.launch
Normal 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>
|
24
Ghidra/Features/Pyhidra/.launch/Pyhidra GUI.launch
Normal file
24
Ghidra/Features/Pyhidra/.launch/Pyhidra GUI.launch
Normal 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 -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>
|
@ -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>
|
24
Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter.launch
Normal file
24
Ghidra/Features/Pyhidra/.launch/Pyhidra Interpreter.launch
Normal 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>
|
22
Ghidra/Features/Pyhidra/.launch/_Pyhidra GUI Debug.launch
Normal file
22
Ghidra/Features/Pyhidra/.launch/_Pyhidra GUI Debug.launch
Normal 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 -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>
|
@ -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>
|
15
Ghidra/Features/Pyhidra/Module.manifest
Normal file
15
Ghidra/Features/Pyhidra/Module.manifest
Normal 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
|
81
Ghidra/Features/Pyhidra/build.gradle
Normal file
81
Ghidra/Features/Pyhidra/build.gradle
Normal 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 }
|
||||
}
|
||||
}
|
||||
|
7
Ghidra/Features/Pyhidra/certification.manifest
Normal file
7
Ghidra/Features/Pyhidra/certification.manifest
Normal 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|
|
21
Ghidra/Features/Pyhidra/data/python.theme.properties
Normal file
21
Ghidra/Features/Pyhidra/data/python.theme.properties
Normal 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]
|
||||
|
77
Ghidra/Features/Pyhidra/ghidra_scripts/PyhidraBasics.py
Normal file
77
Ghidra/Features/Pyhidra/ghidra_scripts/PyhidraBasics.py
Normal 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
|
91
Ghidra/Features/Pyhidra/pyhidraLauncher.py
Normal file
91
Ghidra/Features/Pyhidra/pyhidraLauncher.py
Normal 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()
|
@ -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>
|
@ -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> Move backward in command stack</LI>
|
||||
<LI><B>(down):</B> Move forward in command stack</LI>
|
||||
<LI><B>TAB:</B> Show code completion window</LI>
|
||||
</UL>
|
||||
|
||||
<P>
|
||||
With the code completion window open:
|
||||
<UL>
|
||||
<LI><B>TAB:</B> Insert currently-selected code completion (if no completion selected, select the first available)</LI>
|
||||
<LI><B>ENTER:</B> Insert selected completion (if any) and close the completion window</LI>
|
||||
<LI><B>(up):</B> Select previous code completion</LI>
|
||||
<LI><B>(down):</B> Select next code completion</LI>
|
||||
<LI><B>ESC:</B> 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> CTRL+C / CTRL+V</LI>
|
||||
<LI><B>Linux:</B> CTRL+C / CTRL+V</LI>
|
||||
<LI><B>OS X:</B> 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> </P>
|
||||
<BR>
|
||||
<BR>
|
||||
<BR>
|
||||
|
||||
</BODY>
|
||||
</HTML>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
11
Ghidra/Features/Pyhidra/src/main/py/LICENSE
Normal file
11
Ghidra/Features/Pyhidra/src/main/py/LICENSE
Normal 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.
|
2
Ghidra/Features/Pyhidra/src/main/py/MANIFEST.in
Normal file
2
Ghidra/Features/Pyhidra/src/main/py/MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
graft tests
|
||||
global-exclude *.pyc
|
175
Ghidra/Features/Pyhidra/src/main/py/README.md
Normal file
175
Ghidra/Features/Pyhidra/src/main/py/README.md
Normal 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
|
||||
```
|
57
Ghidra/Features/Pyhidra/src/main/py/pyproject.toml
Normal file
57
Ghidra/Features/Pyhidra/src/main/py/pyproject.toml
Normal 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"]
|
14
Ghidra/Features/Pyhidra/src/main/py/setup.py
Normal file
14
Ghidra/Features/Pyhidra/src/main/py/setup.py
Normal 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()
|
53
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__init__.py
Normal file
53
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__init__.py
Normal 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"
|
||||
]
|
273
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__main__.py
Normal file
273
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/__main__.py
Normal 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()
|
@ -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)
|
351
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/core.py
Normal file
351
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/core.py
Normal 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)
|
103
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidra_launch.py
Normal file
103
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidra_launch.py
Normal 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()
|
175
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidradoc.py
Normal file
175
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/ghidradoc.py
Normal 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
|
148
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/gui.py
Normal file
148
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/gui.py
Normal 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()
|
||||
|
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Internal use only
|
||||
"""
|
@ -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()
|
@ -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)
|
91
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/javac.py
Normal file
91
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/javac.py
Normal 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)
|
711
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/launcher.py
Normal file
711
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/launcher.py
Normal 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
|
@ -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)
|
300
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/script.py
Normal file
300
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/script.py
Normal 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
|
74
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/version.py
Normal file
74
Ghidra/Features/Pyhidra/src/main/py/src/pyhidra/version.py
Normal 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())
|
@ -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 {
|
||||
|
||||
}
|
@ -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
|
@ -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 "";
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
from example_script import import_test_function
|
||||
|
||||
if __name__ == '__main__':
|
||||
import_test_function()
|
@ -0,0 +1,5 @@
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert currentProgram is None
|
||||
assert state.getProject() is not None
|
||||
print("programless_script executed successfully")
|
@ -0,0 +1,5 @@
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert currentProgram is None
|
||||
assert state.getProject() is None
|
||||
print("projectless_script executed successfully")
|
226
Ghidra/Features/Pyhidra/src/main/py/tests/test_argparser.py
Normal file
226
Ghidra/Features/Pyhidra/src/main/py/tests/test_argparser.py
Normal 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"]
|
222
Ghidra/Features/Pyhidra/src/main/py/tests/test_core.py
Normal file
222
Ghidra/Features/Pyhidra/src/main/py/tests/test_core.py
Normal 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
|
167
Ghidra/Features/Pyhidra/src/main/py/tests/test_plugin.py
Normal file
167
Ghidra/Features/Pyhidra/src/main/py/tests/test_plugin.py
Normal 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()
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
0
Ghidra/RuntimeScripts/Linux/support/pythonRun → Ghidra/RuntimeScripts/Linux/support/jythonRun
Executable file → Normal file
0
Ghidra/RuntimeScripts/Linux/support/pythonRun → Ghidra/RuntimeScripts/Linux/support/jythonRun
Executable file → Normal file
33
Ghidra/RuntimeScripts/Linux/support/pyhidraRun
Normal file
33
Ghidra/RuntimeScripts/Linux/support/pyhidraRun
Normal 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} "$@"
|
46
Ghidra/RuntimeScripts/Windows/support/pyhidraRun.bat
Normal file
46
Ghidra/RuntimeScripts/Windows/support/pyhidraRun.bat
Normal 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%
|
@ -43,7 +43,7 @@ rootProject.PLATFORMS.each { platform ->
|
||||
include "gradlew"
|
||||
into "support/gradle"
|
||||
}
|
||||
t.from (p.file("Linux/ghidraRun"))
|
||||
t.from (p.file("Linux/ghidraRun"))
|
||||
}
|
||||
|
||||
if (isWindows(platform.name)) {
|
||||
|
@ -28,4 +28,5 @@ dependencies {
|
||||
}
|
||||
|
||||
rootProject.createJsondocs.dependsOn jar
|
||||
rootProject.createPythonTypeStubs.dependsOn jar
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
@ -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 &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';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ future releases.
|
||||
<li><a href="#RunServer">Ghidra Server</a></li>
|
||||
<li><a href="#RunHeadless">Headless (Batch) Mode</a></li>
|
||||
<li><a href="#RunJar">Single Jar Mode</a></li>
|
||||
<li><a href="#RunPyhidra">Pyhidra</a></li>
|
||||
</ul>
|
||||
<li><a href="#Extensions">Extensions</a></li>
|
||||
<ul>
|
||||
@ -98,8 +99,10 @@ Ghidra team if you have a specific need.</p></blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
<li>Python3 (3.9 to 3.12; for Debugger support)</li>
|
||||
<li>Python3 (3.9 to 3.12)</li>
|
||||
<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
|
||||
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>
|
||||
@ -306,7 +309,7 @@ is complete.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"><b>LICENSE.txt</b></td>
|
||||
<td valign="top"><b>LICENSE</b></td>
|
||||
<td valign="top">Ghidra license information.</td>
|
||||
</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
|
||||
removal of feature sets and also provides many files that can be extended or configured. However,
|
||||
there are times when it would be useful to have all or some subset of Ghidra compressed into a
|
||||
single jar file at the expense of configuration options. This makes Ghidra easier to run from the
|
||||
single jar file at the expense of configuration options. This makes Ghidra easier to run from the
|
||||
command line for headless operation or to use as a library of reverse engineering capabilities for
|
||||
another Java application.</p>
|
||||
<p>A single ghidra.jar file can be created using the
|
||||
<i><GhidraInstallDir></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><GhidraInstallDir></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><GhidraInstallDir></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><GhidraInstallDir></i>/Ghidra/Features/Pyhidra/Pyhidra_README.html file.</p>
|
||||
|
||||
<p>(<a href="#top">Back to Top</a>)</p>
|
||||
|
||||
<h2><a name="Extensions"></a>Extensions</h2>
|
||||
|
@ -111,6 +111,7 @@ apply from: 'gradle/root/distribution.gradle' // adds zip tasks
|
||||
apply from: 'gradle/root/usage.gradle' // adds task documentation
|
||||
apply from: "gradle/root/svg.gradle" // adds task to process svg files
|
||||
apply from: "gradle/root/jacoco.gradle" // adds tasks for java code coverage
|
||||
apply from: "gradle/root/venv.gradle" // adds tasks python virtual environments
|
||||
|
||||
|
||||
apply plugin: 'base'
|
||||
|
@ -183,6 +183,7 @@ plugins.withType(JavaPlugin) {
|
||||
}
|
||||
from (p.projectDir.toString() + "/ghidra_scripts") {
|
||||
exclude 'bin/'
|
||||
exclude '**/__pycache__/**'
|
||||
into { zipPath + "/ghidra_scripts" }
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,9 @@ rootProject.assembleDistribution {
|
||||
exclude '**/*.pyc'
|
||||
exclude '**/*.pyo'
|
||||
exclude '**/__pycache__/**'
|
||||
exclude 'dist/*.tar.gz'
|
||||
exclude '**/.pytest_cache'
|
||||
exclude '**/*.egg-info'
|
||||
exclude 'build'
|
||||
into { zipPath + "/pypkg" }
|
||||
}
|
||||
}
|
||||
|
@ -28,3 +28,7 @@ rootProject.createJavadocs {
|
||||
rootProject.createJsondocs {
|
||||
source sourceSets.main.allJava
|
||||
}
|
||||
|
||||
rootProject.createPythonTypeStubs {
|
||||
source sourceSets.main.allJava
|
||||
}
|
||||
|
@ -194,6 +194,94 @@ task createJsondocs(type: Javadoc, description: 'Generate JSON docs for all proj
|
||||
}
|
||||
}
|
||||
|
||||
task createPythonTypeStubs(type: Javadoc, description: 'Generate pyi stubs for all projects', group: 'Documentation') {
|
||||
|
||||
group 'private'
|
||||
|
||||
String ROOT_PROJECT_DIR = rootProject.projectDir.toString()
|
||||
|
||||
destinationDir file(ROOT_PROJECT_DIR + "/build/typestubs/src")
|
||||
|
||||
failOnError false
|
||||
|
||||
// Must add classpath for main and test source sets. Javadoc will fail if it cannot
|
||||
// find referenced classes.
|
||||
classpath = rootProject.ext.ghidraPath
|
||||
|
||||
|
||||
// Generate at package level because user may try to get help directly on an object they have
|
||||
// rather than its public interface.
|
||||
options.addBooleanOption("package", true)
|
||||
|
||||
// Set the ghidra flag to enable the creation of the ghidra_builtins pseudo package
|
||||
options.addBooleanOption("ghidra", true)
|
||||
|
||||
// Newer versions of gradle set this to true by default.
|
||||
// The JsonDoclet doesn't have the -notimestamp option so ensure it isn't set.
|
||||
options.setNoTimestamp(false)
|
||||
|
||||
// Some internal packages are not public and need to be exported.
|
||||
options.addMultilineStringsOption("-add-exports").setValue(["java.desktop/sun.awt=ALL-UNNAMED"])
|
||||
|
||||
options.doclet = "ghidra.doclets.typestubs.PythonTypeStubDoclet"
|
||||
doFirst {
|
||||
options.docletpath = new ArrayList(configurations.jsondoc.files)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task createGhidraStubsWheel {
|
||||
group 'private'
|
||||
description "Creates the ghidra-stubs wheel for the Ghidra api. [gradle/root/distribution.gradle]"
|
||||
|
||||
dependsOn("createPythonTypeStubs")
|
||||
|
||||
String ROOT_PROJECT_DIR = rootProject.projectDir.toString()
|
||||
|
||||
def cwd = file(ROOT_PROJECT_DIR + "/build/typestubs")
|
||||
def destinationDir = file(cwd.toString() + "/dist")
|
||||
|
||||
it.outputs.file(destinationDir.toString() + "/ghidra_stubs-${project.version}-py3-none-any.whl")
|
||||
|
||||
doFirst {
|
||||
copy {
|
||||
from(file(ROOT_PROJECT_DIR + "/LICENSE"))
|
||||
into cwd
|
||||
}
|
||||
|
||||
def manifest = file(cwd.toString() + "/MANIFEST.in" )
|
||||
manifest.write("graft src\n")
|
||||
|
||||
def pyproject = file(cwd.toString() + "/pyproject.toml" )
|
||||
pyproject.write("""\
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ghidra-stubs"
|
||||
version = "${project.version}"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Typing :: Stubs Only",
|
||||
]
|
||||
""".stripIndent()
|
||||
)
|
||||
}
|
||||
|
||||
doLast {
|
||||
File setuptools = project(":Debugger-rmi-trace").findPyDep(".")
|
||||
exec {
|
||||
workingDir { cwd.toString() }
|
||||
commandLine rootProject.PYTHON3, "-m", "pip"
|
||||
args "wheel", "-w", destinationDir.toString(), "--no-index"
|
||||
args "-f", setuptools
|
||||
args "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********************************************************************************
|
||||
* JAVADOCS - ZIP
|
||||
*
|
||||
@ -336,6 +424,13 @@ task assembleDistribution (type: Copy) {
|
||||
from (zipJavadocs) {
|
||||
into 'docs'
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// Ghidra Python type stubs
|
||||
////////////////////////////
|
||||
from (createGhidraStubsWheel) {
|
||||
into 'docs'
|
||||
}
|
||||
|
||||
////////////////
|
||||
// Patch Readme
|
||||
|
17
gradle/root/venv.gradle
Normal file
17
gradle/root/venv.gradle
Normal 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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user