diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e55d2f..c7b08f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,20 @@
# Changelog
+## [1.2.0] - 2024-05-31
+- Fixed Python interpreter completion results for members of currentAddress, currentProgram, etc.
+- Fixed handling of complex Python interpreter completions.
+- Use configured theme colors for Python interpreter completions.
+- Removed Mac specific pyobjc dependency.
+- Fixed bug causing Mac specific properties from launch.properties to be omitted.
+- Fixed icon bug with the Windows shortcut.
+- Added Mac support to the script for installing a desktop launcher.
+
## [1.1.0] - 2024-04-23
- Improved `pyhidraw` compatibility on Mac.
- Added loader parameter to `open_program` and `run_script` (#37).
- Added script to install a desktop launcher for Windows and Linux. (see [docs](./README.md#desktop-entry))
- Removed `--shortcut` option on `pyhidra` command.
-
## [1.0.2] - 2024-02-14
- Added `--debug` switch to `pyhidra` command line to set the `pyhidra` logging level to `DEBUG`.
- Warnings when compiling Java code are now logged at the `INFO` logging level.
@@ -100,10 +108,11 @@
- Fixed noise produced from an exception during analysis due to an analyzer using a script without acquiring a bundle host reference
- Fixed exception in open_program from attempting to use a non-public field in `FlatProgramAPI`
-## 0.1.0 - 2021-06-14
+## [0.1.0] - 2021-06-14
- Initial release
-[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.1.0...HEAD
+[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.2.0...HEAD
+[1.2.0]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.1.0...1.2.0
[1.1.0]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.2...1.1.0
[1.0.2]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.1...1.0.2
[1.0.1]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.0...1.0.1
diff --git a/README.md b/README.md
index 189cdf9..7ca83e3 100644
--- a/README.md
+++ b/README.md
@@ -29,14 +29,17 @@ pip install pyhidra
### Desktop Entry
-If on linux or windows, a desktop entry can be created to launch an instance of Ghidra with pyhidra attached.
+If on linux, mac or windows, a desktop entry can be created to launch an instance of Ghidra with pyhidra attached.
+When this script is run from a virtual environment (ie. venv), pyhidra will be started in this virtual environment
+when launched.
```console
python -m pyhidra.install_desktop
```
On windows, this will install a shortcut file on the user's desktop. On linux, this will create an entry
-that can be found in the applications launcher.
+that can be found in the applications launcher. On mac this will create an entry that can be found in
+the Launchpad.
To remove, run the following:
diff --git a/pyhidra/__init__.py b/pyhidra/__init__.py
index 99bedfd..a0778ac 100644
--- a/pyhidra/__init__.py
+++ b/pyhidra/__init__.py
@@ -1,5 +1,5 @@
-__version__ = "1.1.0"
+__version__ = "1.2.0"
# Expose API
from .core import run_script, start, started, open_program
diff --git a/pyhidra/install_desktop.py b/pyhidra/install_desktop.py
index b7a6723..0ce61dd 100644
--- a/pyhidra/install_desktop.py
+++ b/pyhidra/install_desktop.py
@@ -26,6 +26,8 @@
from pyhidra.win_shortcut import create_shortcut
elif sys.platform == "linux":
from pyhidra.linux_shortcut import create_shortcut
+ elif sys.platform == "darwin":
+ from pyhidra.mac_shortcut import create_shortcut
else:
sys.exit("Unsupported platform")
diff --git a/pyhidra/java/plugin/completions.py b/pyhidra/java/plugin/completions.py
index ee8f7ab..5635fc5 100644
--- a/pyhidra/java/plugin/completions.py
+++ b/pyhidra/java/plugin/completions.py
@@ -14,16 +14,30 @@
NoneType = type(None)
-CLASS_COLOR = Color(0, 0, 255)
-CODE_COLOR = Color(0, 64, 0)
-FUNCTION_COLOR = Color(0, 128, 0)
-INSTANCE_COLOR = Color(128, 0, 128)
-MAP_COLOR = Color(64, 96, 128)
-METHOD_COLOR = Color(0, 128, 128)
-NULL_COLOR = Color(255, 0, 0)
-NUMBER_COLOR = Color(64, 64, 64)
-PACKAGE_COLOR = Color(128, 0, 0)
-SEQUENCE_COLOR = Color(128, 96, 64)
+try:
+ from generic.theme import GColor
+ 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")
+except:
+ # no custom theme support yet, fall back to hardcoded values
+ CLASS_COLOR = Color(0, 0, 255)
+ CODE_COLOR = Color(0, 64, 0)
+ FUNCTION_COLOR = Color(0, 128, 0)
+ INSTANCE_COLOR = Color(128, 0, 128)
+ MAP_COLOR = Color(64, 96, 128)
+ METHOD_COLOR = Color(0, 128, 128)
+ NULL_COLOR = Color(255, 0, 0)
+ NUMBER_COLOR = Color(64, 64, 64)
+ PACKAGE_COLOR = Color(128, 0, 0)
+ SEQUENCE_COLOR = Color(128, 96, 64)
_TYPE_COLORS = {
type: CLASS_COLOR,
@@ -83,8 +97,9 @@ def _get_label(self, i: int) -> GLabel:
return label
def _supplier(self, i: int) -> CodeCompletion:
- insertion = self.matches[i][len(self.cmd):]
- return CodeCompletion(self.cmd, insertion, self._get_label(i))
+ match = self.matches[i]
+ insertion = match[len(self.cmd):]
+ return CodeCompletion(match, insertion, self._get_label(i))
def get_completions(self, cmd: str):
"""
diff --git a/pyhidra/java/plugin/plugin.py b/pyhidra/java/plugin/plugin.py
index e5a3462..2689704 100644
--- a/pyhidra/java/plugin/plugin.py
+++ b/pyhidra/java/plugin/plugin.py
@@ -1,13 +1,11 @@
import contextlib
import ctypes
-import itertools
import logging
-import rlcompleter
+import re
import sys
import threading
from code import InteractiveConsole
-from ghidra.app.plugin.core.console import CodeCompletion
from ghidra.app.plugin.core.interpreter import InterpreterConsole, InterpreterPanelService
from ghidra.framework import Application
from java.io import BufferedReader, InputStreamReader, PushbackReader
@@ -170,6 +168,8 @@ class PyPhidraPlugin:
"""
The Python side PyhidraPlugin
"""
+
+ _WORD_PATTERN = re.compile(r".*?([\w\.]+)\Z") # get the last word, including '.', from the right
def __init__(self, plugin):
if hasattr(self, '_plugin'):
@@ -224,6 +224,9 @@ def getCompletions(self, *args):
else:
# older versions of Ghidra don't have the `end` argument.
line, = args
+ match = self._WORD_PATTERN.match(line)
+ if match:
+ line = match.group(1)
return self.completer.get_completions(line)
except Exception as e:
if not self._logged_completions_change:
diff --git a/pyhidra/launcher.py b/pyhidra/launcher.py
index cd3d306..462836b 100644
--- a/pyhidra/launcher.py
+++ b/pyhidra/launcher.py
@@ -1,4 +1,6 @@
import contextlib
+import ctypes
+import ctypes.util
import importlib.metadata
import inspect
import logging
@@ -121,6 +123,8 @@ def __init__(self, verbose=False, *, install_dir: Path = None):
@classmethod
def _jvm_args(cls, install_dir: Path) -> List[str]:
suffix = "_" + platform.system().upper()
+ if suffix == "_DARWIN":
+ suffix = "_MACOS"
option_pattern: re.Pattern = re.compile(fr"VMARGS(?:{suffix})?=(.+)")
properties = []
@@ -554,7 +558,6 @@ def _get_thread(name: str):
return None
def _launch(self):
- import ctypes
from ghidra import Ghidra
from java.lang import Runtime, Thread
@@ -565,10 +568,27 @@ def _launch(self):
stdout = _PyhidraStdOut(sys.stdout)
stderr = _PyhidraStdOut(sys.stderr)
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
- jpype.setupGuiEnvironment(lambda: Ghidra.main(["ghidra.GhidraRun", *self.args]))
+ Thread(lambda: Ghidra.main(["ghidra.GhidraRun", *self.args])).start()
is_exiting = threading.Event()
Runtime.getRuntime().addShutdownHook(Thread(is_exiting.set))
- try:
- is_exiting.wait()
- finally:
- jpype.shutdownGuiEnvironment()
+ if sys.platform == "darwin":
+ _run_mac_app()
+ is_exiting.wait()
+
+
+def _run_mac_app():
+ # this runs the main event loop
+ # it is required for the GUI to show up
+ objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("libobjc"))
+ ctypes.cdll.LoadLibrary(ctypes.util.find_library("AppKit")) # required
+ msgSend = objc.objc_msgSend
+ msgSend.restype = ctypes.c_void_p
+ msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
+ registerName = objc.sel_registerName
+ registerName.restype = ctypes.c_void_p
+ registerName.argtypes = [ctypes.c_char_p]
+ getClass = objc.objc_getClass
+ getClass.restype = ctypes.c_void_p
+ NSApplication = getClass(b"NSApplication")
+ sharedApplication = msgSend(NSApplication, registerName(b"sharedApplication"))
+ msgSend(sharedApplication, registerName(b"run"))
diff --git a/pyhidra/linux_shortcut.py b/pyhidra/linux_shortcut.py
index 6db5736..159982e 100644
--- a/pyhidra/linux_shortcut.py
+++ b/pyhidra/linux_shortcut.py
@@ -38,12 +38,14 @@ def extract_png(install_dir: Path) -> Path:
def create_shortcut(install_dir: Path = None):
"""Install a desktop entry on Linux machine."""
pyhidra_exec = Path(sysconfig.get_path("scripts")) / "pyhidra"
+ if not pyhidra_exec.exists():
+ # User install
+ pyhidra_exec = Path(sysconfig.get_path("scripts", "posix_user")) / "pyhidra"
if not pyhidra_exec.exists():
sys.exit("pyhidra executable is not installed.")
- command = [str(pyhidra_exec), "--gui"]
if install_dir:
- command += ["--install-dir", str(install_dir.expanduser())]
+ pass
elif install_dir := os.environ.get("GHIDRA_INSTALL_DIR"):
install_dir = Path(install_dir)
else:
@@ -51,8 +53,11 @@ def create_shortcut(install_dir: Path = None):
"Unable to determine Ghidra installation directory. "
"Please set the GHIDRA_INSTALL_DIR environment variable."
)
+
+ command = [str(pyhidra_exec), "--gui", "--install-dir", str(install_dir.expanduser())]
icon = extract_png(install_dir)
+ desktop_path.parent.mkdir(parents=True, exist_ok=True)
desktop_path.write_text(desktop_entry.format(icon=icon, exec=shlex.join(command)))
print(f"Installed {desktop_path}")
diff --git a/pyhidra/mac_shortcut.py b/pyhidra/mac_shortcut.py
new file mode 100644
index 0000000..89d8151
--- /dev/null
+++ b/pyhidra/mac_shortcut.py
@@ -0,0 +1,186 @@
+from base64 import b64encode
+from functools import cached_property
+from hashlib import sha1, sha256
+import os
+from pathlib import Path
+import shutil
+import stat
+import subprocess
+import sys
+from tempfile import TemporaryDirectory
+
+from pyhidra.linux_shortcut import extract_png
+
+
+applications = Path("~/Applications").expanduser()
+
+
+class AppBuilder:
+
+ APP_NAME = "Ghidra (pyhidra).app"
+ ICON_NAME = "ghidra.icns"
+
+ def __init__(self, install_dir: Path):
+ self._tmpdir = TemporaryDirectory()
+ self.tmpdir = Path(self._tmpdir.name)
+ self.install_dir = install_dir
+
+ @property
+ def desktop_path(self) -> Path:
+ app_dir = applications
+ app_dir.mkdir(exist_ok=True)
+ return app_dir / self.APP_NAME
+
+ @cached_property
+ def contents(self) -> Path:
+ return self.tmpdir / self.APP_NAME / "Contents"
+
+ @cached_property
+ def icon_path(self) -> Path:
+ return self.contents / "Resources" / self.ICON_NAME
+
+ def create_icon(self):
+ icon_dir = self.tmpdir / "ghidra.iconset"
+ icon_dir.mkdir()
+ extract_png(self.install_dir).rename(icon_dir / "icon_256x256.png")
+ cmd = ["/usr/bin/iconutil", "--convert", "icns", "ghidra.iconset"]
+ subprocess.check_call(cmd, cwd=self.tmpdir)
+ resources = self.contents / "Resources"
+ resources.mkdir(parents=True)
+ icon = self.tmpdir / self.ICON_NAME
+ icon.rename(self.icon_path)
+
+ def create_code_resources(self):
+ icon_data = self.icon_path.read_bytes()
+ hash1 = b64encode(sha1(icon_data).digest()).decode("utf-8")
+ hash2 = b64encode(sha256(icon_data).digest()).decode("utf-8")
+ sigdir = self.contents / "_CodeSignature"
+ sigdir.mkdir(parents=True)
+ code_resources = sigdir / "CodeResources"
+ data = CODE_RESOURCES.format(hash1=hash1, hash2=hash2)
+ code_resources.write_text(data, encoding="utf-8")
+
+ def create_info_plist(self):
+ info_plist = self.contents / "Info.plist"
+ data = INFO_PLIST.format(install_dir=self.install_dir)
+ info_plist.write_text(data, encoding="utf-8")
+
+ def create_exe(self):
+ exe_dir = self.contents / "MacOS"
+ exe_dir.mkdir(parents=True)
+ script_path = exe_dir / "pyhidra"
+
+ # NOTE: using sys.executable allows venv to work properly
+ data = PYHIDRA_SCRIPT.format(python=sys.executable)
+ script_path.write_text(data, encoding="utf-8")
+
+ # chmod +x
+ mode = script_path.stat().st_mode | stat.S_IXUSR
+ script_path.chmod(mode)
+
+ def move(self):
+ # remove the existing one first if present
+ if self.desktop_path.exists():
+ shutil.rmtree(self.desktop_path)
+ app_dir = self.tmpdir / self.APP_NAME
+ app_dir.rename(self.desktop_path)
+
+ def __enter__(self):
+ self._tmpdir.__enter__()
+ return self
+
+ def __exit__(self, *args):
+ return self._tmpdir.__exit__(*args)
+
+
+def create_shortcut(install_dir: Path = None):
+ """Install a desktop entry on Mac machine."""
+ if install_dir is None:
+ install_dir = os.environ.get("GHIDRA_INSTALL_DIR")
+ if install_dir is None:
+ sys.exit(
+ "Unable to determine Ghidra installation directory. "
+ "Please set the GHIDRA_INSTALL_DIR environment variable."
+ )
+ install_dir = Path(install_dir)
+
+ with AppBuilder(install_dir) as builder:
+ builder.create_icon()
+ builder.create_code_resources()
+ builder.create_info_plist()
+ builder.create_exe()
+ builder.move()
+
+
+def remove_shortcut():
+ desktop_path = applications / AppBuilder.APP_NAME
+ if desktop_path.exists():
+ shutil.rmtree(desktop_path)
+ print(f"Removed {desktop_path}")
+
+
+CODE_RESOURCES = """
+
+
+
+ files
+
+ Resources/ghidra.icns
+
+ {hash1}
+
+
+ files2
+
+ Resources/ghidra.icns
+
+ hash
+
+ {hash1}
+
+ hash2
+
+ {hash2}
+
+
+
+
+
+"""
+
+INFO_PLIST = """
+
+
+
+ CFBundleExecutable
+ pyhidra
+ CFBundleGetInfoString
+ Ghidra (pyhidra)
+ CFBundleIconFile
+ ghidra.icns
+ CFBundleIdentifier
+ ghidra.Ghidra
+ CFBundleName
+ Ghidra
+ CFBundlePackageType
+ APPL
+ LSEnvironment
+
+ GHIDRA_INSTALL_DIR
+ {install_dir}
+
+ LSMultipleInstancesProhibited
+
+
+
+"""
+
+PYHIDRA_SCRIPT = """#!{python}
+# -*- coding: utf-8 -*-
+import pyhidra.gui
+
+
+if __name__ == '__main__':
+ pyhidra.gui.gui()
+
+"""
\ No newline at end of file
diff --git a/pyhidra/script.py b/pyhidra/script.py
index 70ad9ad..b2dbd5e 100644
--- a/pyhidra/script.py
+++ b/pyhidra/script.py
@@ -17,6 +17,7 @@
class _StaticMap(dict):
+ # this is a special view of the PyGhidraScript for use with rlcompleter
__slots__ = ('script',)
@@ -27,6 +28,14 @@ def __init__(self, script: "PyGhidraScript"):
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)
diff --git a/pyhidra/uninstall_desktop.py b/pyhidra/uninstall_desktop.py
index 15e7fcd..f090ad0 100644
--- a/pyhidra/uninstall_desktop.py
+++ b/pyhidra/uninstall_desktop.py
@@ -7,6 +7,8 @@
from pyhidra.win_shortcut import remove_shortcut
elif sys.platform == "linux":
from pyhidra.linux_shortcut import remove_shortcut
+ elif sys.platform == "darwin":
+ from pyhidra.mac_shortcut import remove_shortcut
else:
sys.exit("Unsupported platform")
diff --git a/pyhidra/win_shortcut.py b/pyhidra/win_shortcut.py
index 1b1c428..12b3b13 100644
--- a/pyhidra/win_shortcut.py
+++ b/pyhidra/win_shortcut.py
@@ -51,7 +51,7 @@ def __init__(self, key: str, pid: int) -> None:
_Save = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p, ctypes.wintypes.BOOL)(6, "Save")
_SetPath = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(20, "SetPath")
_SetDescription = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(7, "SetDescription")
- _SetIconLocation = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(17, "SetIconLocation")
+ _SetIconLocation = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p, ctypes.c_int)(17, "SetIconLocation")
_SetValue = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.c_void_p)(6, "SetValue")
link = str(link)
@@ -67,7 +67,7 @@ def __init__(self, key: str, pid: int) -> None:
_CoCreateInstance(_CLSID_ShellLink, None, _CLSCTX_INPROC_SERVER, _IID_IShellLinkW, ref)
_SetPath(p_link, ctypes.c_wchar_p(str(target)))
_SetDescription(p_link, p_app_id)
- _SetIconLocation(p_link, ctypes.c_wchar_p(icon))
+ _SetIconLocation(p_link, ctypes.c_wchar_p(icon), 0)
_QueryInterface(p_link, _IID_IPropertyStore, ctypes.byref(p_store))
value = _PropertyVariant.pack(_VT_LPWSTR, ctypes.cast(p_app_id, ctypes.c_void_p).value)
value = (ctypes.c_byte * len(value))(*value)
diff --git a/setup.cfg b/setup.cfg
index e487f0f..c87a186 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,7 +29,6 @@ zip_safe = False
include_package_data = True
install_requires =
Jpype1>=1.3.0; python_version < '3.12'
- pyobjc; sys_platform == "darwin"
Jpype1>=1.5.0; python_version >= '3.12'
[options.entry_points]