diff --git a/Ghidra/Features/PyGhidra/src/main/py/README.md b/Ghidra/Features/PyGhidra/src/main/py/README.md index d292da545a..0e4c5c9b2e 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/README.md +++ b/Ghidra/Features/PyGhidra/src/main/py/README.md @@ -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). diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py index 0121f52864..54b46c16e9 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py @@ -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 diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py index 0f17bafef5..a4c131459f 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py @@ -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) diff --git a/Ghidra/Features/PyGhidra/src/main/py/tests/test_core.py b/Ghidra/Features/PyGhidra/src/main/py/tests/test_core.py index 6feb9aa440..9f2ae147eb 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/tests/test_core.py +++ b/Ghidra/Features/PyGhidra/src/main/py/tests/test_core.py @@ -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: