GP-6754: Jython is now an extension

This commit is contained in:
Ryan Kurtz
2026-04-24 06:06:24 -04:00
parent 4a9b40c453
commit f14617d2eb
49 changed files with 23 additions and 10 deletions
@@ -0,0 +1,190 @@
## ###
# 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 __builtin__
import java
import re
import json
import os
import zipfile
from ghidra.framework import Application
from ghidra.util import SystemUtilities
class _Helper:
def __init__(self):
self.orig_help = __builtin__.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:
# PythonPlugin 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 PythonPlugin scenario to show help for GhidraScript
return "ghidra.app.script.GhidraScript", None
class_name = None
method_name = None
if type(param) in [type(1), type(1j), type(1L), type(1.0), type(None), type(True), type([]), type({}), type(()), type({1})]:
# These are instances of builtin types, so skip
pass
elif type(param) == type(str):
# These are builtin Python types, so skip
pass
elif type(param) == type(str.split):
# These are python functions, so skip
pass
elif type(param) == type(java):
# These are java packages, which we don't don't document, so skip
pass
elif type(param) == type(java.lang.Object):
# This is a java class, so extract its class name
match = re.search("'(.*)'", str(param))
if match is not None:
class_name = match.group(1)
elif type(param) == type(java.lang.Object().toString):
# This is a java method, so extract its class name and method name
tokens = str(param).split(" ")[2].split(".")
class_name = ".".join(tokens[:-1])
method_name = tokens[-1]
else:
# Assuming this is a java object, so extract its class name
match = re.search("'(.*)'", str(type(param)))
if match is not None:
class_name = match.group(1)
return class_name, method_name
def get_jsondoc(class_name):
jsondoc = None
try:
root = Application.getApplicationRootDirectory().getFile(False).getParentFile().getAbsolutePath()
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 os.path.exists(javadoc_zip):
json_path = "api/" + class_name.replace('.', '/') + '.json'
with zipfile.ZipFile(javadoc_zip, "r").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 = ""
for interface in cls["implements"]:
if len(implements) > 0:
implements += ", "
implements += interface
if len(implements) > 0:
sig += " implements " + implements + " \n"
sig += "\n" + cls["comment"]
return sig
def format_field(field):
sig = "%s %s" % (field["type_long"], field["name"])
if field["static"]:
sig = "static " + sig
if field["constant_value"]:
sig += " = " + field["constant_value"]
sig += "\n"
desc = " %s\n" % (field["comment"]) if len(field["comment"]) > 0 else ""
return sig + desc
def format_method(method):
paramsig = ""
args = ""
for param in method["params"]:
if len(paramsig) > 0:
paramsig += ", "
paramsig += "%s %s" % (param["type_short"], param["name"])
args += " @param %s (%s): %s\n" % (param["name"], param["type_long"], param["comment"])
throws = ""
for exception in method["throws"]:
throws += " @throws %s: %s\n" % (exception["type_short"], exception["comment"])
sig = "%s %s(%s)\n" % (method["return"]["type_short"], method["name"], paramsig)
if method["static"]:
sig = "static " + sig
desc = " %s\n\n" % (method["comment"]) if len(method["comment"]) > 0 else ""
ret = ""
if method["return"]["type_short"] != "void":
ret = " @return %s: %s\n" % (method["return"]["type_long"], method["return"]["comment"])
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
print "Searching API for " + class_name + ("" if method_name is None else "." + method_name + "()") + "..."
jsondoc = get_jsondoc(class_name)
if jsondoc is None:
print "No API found for " + class_name
elif method_name is None:
print "#####################################################"
print format_class(jsondoc)
print "#####################################################\n"
for field in jsondoc["fields"]:
print format_field(field)
print "-----------------------------------------------------"
for method in jsondoc["methods"]:
print format_method(method)
print "-----------------------------------------------------"
else:
found_method = False
for method in jsondoc["methods"]:
if method["name"] == method_name:
print "-----------------------------------------------------"
print format_method(method)
print "-----------------------------------------------------"
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
__builtin__.help = _Helper()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,237 @@
## ###
# IP: LGPL 2.1
##
"""Extend introspect.py for Java based Jython classes."""
from introspect import *
import string
import __builtin__
import java # needed for java.lang.Class
import org # for org.python.core
import ghidra # for JythonCodeCompletionFactory
__author__ = "Don Coleman <dcoleman@chariotsolutions.com>"
#def getAutoCompleteList(command='', locals=None, includeMagic=1,
# includeSingle=1, includeDouble=1):
# """Return list of auto-completion options for command.
#
# The list of options will be based on the locals namespace."""
# attributes = []
# # Get the proper chunk of code from the command.
# root = getRoot(command, terminator='.')
# try:
# if locals is not None:
# object = eval(root, locals)
# else:
# object = eval(root)
# except:
# #print "could not eval(", root, "):", sys.exc_info()[0]
# return attributes
#
# if ispython(object):
# # use existing code
# attributes = getAttributeNames(object, includeMagic, includeSingle,
# includeDouble)
# else:
# methods = methodsOf(object.__class__)
# attributes = [eachMethod.__name__ for eachMethod in methods]
#
# return attributes
#
#def methodsOf(clazz):
# """Return a list of all the methods in a class"""
# classMembers = vars(clazz).values()
# methods = [eachMember for eachMember in classMembers
# if callable(eachMember)]
# for eachBase in clazz.__bases__:
# methods.extend(methodsOf(eachBase))
# return methods
def getCallTipJava(command='', locals=None):
"""For a command, return a tuple of object name, argspec, tip text.
The call tip information will be based on the locals namespace."""
calltip = ('', '', '') # object name, argspec, tip text.
# Get the proper chunk of code from the command.
(root, filter) = getRootAndFilter(command, terminator='(')
#java.lang.System.out.println("root=" + root)
try:
if locals is not None:
object = eval(root, locals)
else:
object = eval(root)
except:
#java.lang.System.err.println("could not eval(" + root + "):" +
# str(sys.exc_info()[0]))
return calltip
if ispython(object):
# Patrick's code handles Python code
# TODO fix in future because getCallTip runs eval() again
#java.lang.System.out.println("is a Python object")
calltip = getCallTip(command, locals)
if not calltip[1] and not calltip[2]:
# either it's a pure Java object, or we didn't get much from Python's
# getCallTip
name = ''
try:
name = object.__name__
except AttributeError:
pass
tipList = []
argspec = '' # not using argspec for Java
# if inspect.isbuiltin(object):
# # inspect.isbuiltin() fails for Jython
# # Can we get the argspec for Jython builtins? We can't in Python.
# # YES!
# print "is a builtin"
# pass
# elif inspect.isclass(object):
if inspect.isclass(object):
# get the constructor(s)
# TODO consider getting modifiers since Jython can access
# private methods
#java.lang.System.out.println("is a class")
try:
# this will likely fail for pure Python classes
constructors = object.getConstructors()
for constructor in constructors:
paramList = []
paramTypes = constructor.getParameterTypes()
# paramTypes is an array of classes; we need Strings
# TODO consider list comprehension
for param in paramTypes:
# TODO translate [B to byte[], C to char[], etc.
paramList.append(param.__name__)
paramString = string.join(paramList, ', ')
tip = "%s(%s)" % (constructor.name, paramString)
tipList.append(tip)
if len(constructors) == 1:
plural = ""
else:
plural = "s"
name = "Constructor" + plural + " for " + name + ":"
except:
pass
# if callable(object):
# # some Python types are function names as well, like
# # type() and file()
# argspec = str(object.__call__)
# # these don't seem to be very accurate
# if hasattr(object.__call__, "maxargs"):
# tipList.append("maxargs?: " +
# str(object.__call__.maxargs))
# if hasattr(object.__call__, "minargs"):
# tipList.append("minargs?: " +
# str(object.__call__.minargs))
# elif inspect.ismethod(object):
elif inspect.isroutine(object):
#java.lang.System.out.println("is a routine")
# method = object
# object = method.im_class
#
# # Java allows overloading so we may have more than one method
# methodArray = object.getMethods()
#
# for eachMethod in methodArray:
# if eachMethod.name == method.__name__:
# paramList = []
# for eachParam in eachMethod.parameterTypes:
# paramList.append(eachParam.__name__)
#
# paramString = string.join(paramList, ', ')
#
# # create a Python style string a la PyCrust
# # we're showing the parameter type rather than the
# # parameter name, since that's all we can get
# # we need to show multiple methods for overloading
# # TODO improve message format
# # do we want to show the method visibility?
# # how about exceptions?
# # note: name, return type and exceptions same for
# # EVERY overloaded method
#
#
# tip = "%s(%s) -> %s" % (eachMethod.name, paramString,
# eachMethod.returnType)
# tipList.append(tip)
if hasattr(object, "argslist"):
for args in object.argslist:
if args is not None:
# for now
tipList.append(str(args.method))
# elif callable(object):
# argspec = str(object.__call__)
# # these don't seem to be very accurate
# if hasattr(object.__call__, "maxargs"):
# tipList.append("maxargs?: " + str(object.__call__.maxargs))
# if hasattr(object.__call__, "minargs"):
# tipList.append("minargs?: " + str(object.__call__.minargs))
# elif inspect.isfunction(object):
# print "is function"
if (len(tipList) == 0):
if hasattr(object, "__name__") and \
hasattr(__builtin__, object.__name__):
# try to get arguments for any other "old-style" builtin
# functions (see __builtin__.java, classDictInit() method)
methods = \
java.lang.Class.getMethods(org.python.core.__builtin__)
for method in methods:
if method.name == object.__name__:
tipList.append(str(method))
argspec = "a built-in Python function"
else:
# last-ditch: try possible __call__ methods of new-style
# objects
for possible_call_method in \
ghidra.python.PythonCodeCompletionFactory.getCallMethods(object):
signature = str(possible_call_method)
# clean up the method signature a bit, so it looks sane
signature = \
signature.replace("$1exposed_", ".").replace(".__call__", "")
tipList.append(signature)
calltip = (name, argspec, string.join(tipList, "\n"))
return calltip
def ispython(object):
"""
Figure out if this is Python code or Java code..
"""
pyclass = 0
pycode = 0
pyinstance = 0
if inspect.isclass(object):
try:
object.__doc__
pyclass = 1
except AttributeError:
pyclass = 0
elif inspect.ismethod(object):
try:
# changed for Jython 2.2a1
#object.__dict__
object.__str__
pycode = 1
except AttributeError:
pycode = 0
else: # I guess an instance of an object falls here
try:
# changed for Jython 2.2a1
#object.__dict__
object.__str__
pyinstance = 1
except AttributeError:
pyinstance = 0
return pyclass | pycode | pyinstance
@@ -0,0 +1,48 @@
## ###
# 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.
##
"""
User-supplied customizations go here.
"""
# nice-to-have: place 'java' and 'ghidra' into the local namespace, so you
# can do fun things like "dir(java.lang)"
import java
import ghidra
import __main__
__main__.java = java
__main__.ghidra = ghidra
# fix Jython "bug": unknown type 'javainstance' or 'javapackage' even though
# that is the type Jython gives us if we ask type(<someObject>) or
# type(ghidra) (respectively)
import __builtin__
import org.python.core
# changed by Jim 20090528 for Jython 2.5
# not sure why I even put these here... the first one might be troublesome
#__builtin__.javainstance = org.python.core.PyJavaInstance
#__builtin__.javapackage = org.python.core.PyJavaPackage
#__builtin__.javaclass = org.python.core.PyJavaClass
__builtin__.javainstance = org.python.core.PyObjectDerived
__builtin__.javapackage = org.python.core.PyJavaPackage
__builtin__.javaclass = org.python.core.PyJavaType
# changed by Jim 20090528 for Jython 2.5
# REMOVED collections stuff
# OOPS still need this
import sys
# Ghidra documentation
import ghidradoc