Merge branch

'GP-7301_ryanmkurtz_PR-9642_Bortlesboat_fix_pyghidra-write-only-globals'
(Closes #8978, Closes #9642)
This commit is contained in:
Ryan Kurtz
2026-09-22 08:38:25 -04:00
4 changed files with 83 additions and 4 deletions

View File

@@ -567,6 +567,9 @@ import pdb # imports Python's pdb
import pdb_ # imports Ghidra's pdb
```
## Change History
__3.3.0__
* Eliminated `KeyError` exceptions that were generated while stepping with a debugger.
__3.2.0__
* PyGhidra's `help()` override can now show `public/private/protected` on Ghidra Java fields and
methods (available in Ghidra 12.2 and later).

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
__version__ = "3.2.0"
__version__ = "3.3.0"
# stub for documentation and typing
# this is mostly to hide the function parameter

View File

@@ -51,7 +51,8 @@ class _StaticMap(dict):
# this is necessary for completions on currentAddress, currentProgram, etc.
try:
return getattr(self.script, key)
except AttributeError:
except Exception:
# getters like getFirstFunction() throw when there is no program
return res
return res
raise KeyError(key)
@@ -61,7 +62,9 @@ class _StaticMap(dict):
return res if res is not _NO_ATTRIBUTE else default
def __iter__(self):
yield from self.script
# Completions include the bean properties that regular iteration leaves out.
yield from dict.__iter__(self.script)
yield from dir(self.script._script)
def keys(self):
return KeysView(self)
@@ -194,7 +197,15 @@ class PyGhidraScript(dict):
def __iter__(self):
yield from super().__iter__()
yield from dir(self._script)
for name in dir(self._script):
attr = inspect.getattr_static(self._script, name, _NO_ATTRIBUTE)
# JPype bean properties may have no getter, or a getter that throws when
# there is no program (firstFunction, memoryBlocks, ...). Keep them out of
# iteration so debuggers inspecting globals don't evaluate them; they can
# still be read by name. Exposed fields use _JavaProperty and are kept.
if type(attr) is property:
continue
yield name
def get_static(self, key):
res = self.get(key, _NO_ATTRIBUTE)

View File

@@ -171,6 +171,71 @@ def test_import_script(capsys, shared_datadir: Path):
assert captured.out.rstrip() == "imported successfully"
def test_script_globals_iteration():
pyghidra.start()
from pyghidra.core import _setup_script
# no program is open, which is when getters like getFirstFunction() throw
script = _setup_script(None, None)
script["user_variable"] = 42
names = list(script)
# a debugger reads every name it iterates
for name in names:
script[name]
for name in ("potentialPropertiesFileLocations", "propertiesFile", "referencePrimary"):
assert name not in names
with pytest.raises(KeyError):
script[name]
for name in ("firstFunction", "memoryBlocks", "scriptName"):
assert name not in names
assert script["scriptName"] is not None
for name in ("user_variable", "currentProgram", "currentAddress", "monitor", "getCurrentProgram"):
assert name in names
assert script["user_variable"] == 42
assert script["currentProgram"] is None
assert script["currentAddress"] is None
assert script["monitor"] is not None
assert callable(script["getCurrentProgram"])
def test_script_static_view_includes_bean_properties():
pyghidra.start()
from pyghidra.script import PyGhidraScript
view = PyGhidraScript().get_static_view()
names = list(view)
for name in ("potentialPropertiesFileLocations", "propertiesFile", "referencePrimary"):
assert name in names
assert isinstance(view[name], property)
for name in ("firstFunction", "scriptName"):
assert name in names
def test_script_completion_without_program():
pyghidra.start()
from rlcompleter import Completer
from pyghidra.core import _setup_script
# rlcompleter reads every value in the namespace, including firstFunction
view = _setup_script(None, None).get_static_view()
assert Completer(view).complete("currentPr", 0) == "currentProgram"
def test_script_write_only_property_assignment():
pyghidra.start()
from java.util import ArrayList
from pyghidra.script import PyGhidraScript
script = PyGhidraScript()
locations = ArrayList()
script["potentialPropertiesFileLocations"] = locations
assert script["potentialPropertiesFileLocs"] == locations
def test_import_ghidra_base_java_packages():
def get_runtime_top_level_java_packages(launcher) -> set: