mirror of
https://github.com/gaasedelen/lighthouse
synced 2026-06-08 14:19:10 +00:00
388 lines
13 KiB
Python
388 lines
13 KiB
Python
import os
|
|
import logging
|
|
import weakref
|
|
|
|
from lighthouse.util.qt import *
|
|
from lighthouse.util.misc import plugin_resource
|
|
from lighthouse.util.disassembler import disassembler
|
|
|
|
from lighthouse.composer import ComposingShell
|
|
from lighthouse.ui.coverage_table import CoverageTableView, CoverageTableModel, CoverageTableController
|
|
from lighthouse.ui.coverage_combobox import CoverageComboBox
|
|
from lighthouse.ui.coverage_settings import TableSettingsMenu
|
|
|
|
logger = logging.getLogger("Lighthouse.UI.Overview")
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Coverage Overview
|
|
#------------------------------------------------------------------------------
|
|
|
|
class CoverageOverview(object):
|
|
"""
|
|
The Coverage Overview Widget.
|
|
"""
|
|
|
|
def __init__(self, lctx, widget):
|
|
self.lctx = lctx
|
|
self.widget = widget
|
|
self.director = self.lctx.director
|
|
|
|
self.lctx.coverage_overview = self
|
|
self.initialized = False
|
|
self._refreshed_callback = None
|
|
|
|
# see the EventProxy class below for more details
|
|
self._events = EventProxy(self)
|
|
self.widget.installEventFilter(self._events)
|
|
# plugin_resource(os.path.join("icons", "overview.png"))
|
|
|
|
# initialize the plugin UI
|
|
self._ui_init()
|
|
|
|
# refresh the data UI such that it reflects the most recent data
|
|
self.refresh()
|
|
|
|
# register for cues from the director
|
|
self._refreshed_callback = self.director.refreshed(self.refresh)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Pseudo Widget Functions
|
|
#--------------------------------------------------------------------------
|
|
|
|
@property
|
|
def name(self):
|
|
if not self.widget:
|
|
return "Coverage Overview"
|
|
return self.widget.name
|
|
|
|
@property
|
|
def visible(self):
|
|
if not self.widget:
|
|
return False
|
|
return self.widget.visible
|
|
|
|
def terminate(self):
|
|
"""
|
|
The CoverageOverview is being hidden / deleted.
|
|
"""
|
|
if self.widget is None:
|
|
return
|
|
|
|
if self._refreshed_callback:
|
|
self.director.unregister_refreshed(self._refreshed_callback)
|
|
self._refreshed_callback = None
|
|
|
|
if self.lctx.coverage_overview is self:
|
|
self.lctx.coverage_overview = None
|
|
|
|
self.initialized = False
|
|
self._combobox = None
|
|
self._shell = None
|
|
self._toolbar = None
|
|
self._table_view = None
|
|
self._table_controller = None
|
|
self._table_model = None
|
|
self._settings_button = None
|
|
self._settings_menu = None
|
|
self._shell_elements = None
|
|
self._events = None
|
|
self.widget = None
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Initialization - UI
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _ui_init(self):
|
|
"""
|
|
Initialize UI elements.
|
|
"""
|
|
|
|
# initialize our ui elements
|
|
self._ui_init_table()
|
|
self._ui_init_toolbar()
|
|
self._ui_init_signals()
|
|
|
|
# layout the populated ui just before showing it
|
|
self._ui_layout()
|
|
|
|
def _ui_init_table(self):
|
|
"""
|
|
Initialize the coverage table.
|
|
"""
|
|
self._table_model = CoverageTableModel(self.lctx, self.widget)
|
|
self._table_controller = CoverageTableController(self.lctx, self._table_model)
|
|
self._table_view = CoverageTableView(self._table_controller, self._table_model, self.widget)
|
|
|
|
def _ui_init_toolbar(self):
|
|
"""
|
|
Initialize the coverage toolbar.
|
|
"""
|
|
|
|
# initialize child elements to go on the toolbar
|
|
self._ui_init_toolbar_elements()
|
|
self._ui_init_settings()
|
|
|
|
#
|
|
# create the 'toolbar', and customize its style. specifically, we are
|
|
# interested in tweaking the separator and padding between elements.
|
|
#
|
|
|
|
self._toolbar = QtWidgets.QToolBar()
|
|
self._toolbar.setStyle(QtWidgets.QStyleFactory.create("Windows"))
|
|
self._toolbar.setStyleSheet('QToolBar{padding:0;margin:0;}')
|
|
|
|
# populate the toolbar with all our subordinates
|
|
self._toolbar.addWidget(self._shell_elements)
|
|
self._toolbar.addWidget(self._settings_button)
|
|
|
|
def _ui_init_toolbar_elements(self):
|
|
"""
|
|
Initialize the coverage toolbar UI elements.
|
|
"""
|
|
|
|
# the composing shell
|
|
self._shell = ComposingShell(
|
|
self.lctx,
|
|
weakref.proxy(self._table_model),
|
|
weakref.proxy(self._table_view)
|
|
)
|
|
|
|
# the coverage combobox
|
|
self._combobox = CoverageComboBox(self.director)
|
|
|
|
# the splitter to make the shell / combobox resizable
|
|
self._shell_elements = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
|
|
self._shell_elements.setStyleSheet(
|
|
"""
|
|
QSplitter
|
|
{
|
|
border: none;
|
|
}
|
|
|
|
QSplitter::handle
|
|
{
|
|
background-color: #909090;
|
|
width: 2px;
|
|
height: 2px;
|
|
margin: 0 0.5em 0 0.5em
|
|
}
|
|
|
|
QSplitter::handle:horizontal:hover
|
|
{
|
|
background-color: #3399FF;
|
|
}
|
|
""")
|
|
|
|
# add the child items we wish to put the 'splitter' between
|
|
# [ composing shell ] [SPLITTER] [ combobox ]
|
|
self._shell_elements.addWidget(self._shell)
|
|
self._shell_elements.addWidget(self._combobox)
|
|
|
|
# make the splitter responsive (animate) when hovered
|
|
self._shell_elements.handle(1).setAttribute(QtCore.Qt.WA_Hover)
|
|
|
|
# give the shell expansion preference over the combobox
|
|
self._shell_elements.setStretchFactor(0, 1)
|
|
|
|
def _ui_init_settings(self):
|
|
"""
|
|
Initialize the overview settings popup.
|
|
"""
|
|
|
|
# settings button
|
|
self._settings_button = QtWidgets.QToolButton()
|
|
self._settings_button.setIcon(get_qt_icon("SP_DialogResetButton"))
|
|
self._settings_button.setStyleSheet("QToolButton::menu-indicator{image: none;}")
|
|
|
|
# settings menu
|
|
self._settings_menu = TableSettingsMenu(self.widget)
|
|
|
|
def _ui_init_signals(self):
|
|
"""
|
|
Connect UI signals.
|
|
"""
|
|
self._settings_menu.connect_signals(self._table_controller, self.lctx)
|
|
self._settings_button.clicked.connect(self._ui_show_settings)
|
|
|
|
def _ui_layout(self):
|
|
"""
|
|
Layout the major UI elements of the widget.
|
|
"""
|
|
|
|
# layout the major elements of our widget
|
|
layout = QtWidgets.QGridLayout()
|
|
layout.setSpacing(int(get_dpi_scale()*5))
|
|
layout.addWidget(self._table_view)
|
|
layout.addWidget(self._toolbar)
|
|
|
|
# apply the layout to the containing form
|
|
self.widget.setLayout(layout)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Signal Handlers
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _ui_show_settings(self):
|
|
"""
|
|
Handle a click of the settings button.
|
|
"""
|
|
delta = QtCore.QPoint(
|
|
-1*self._settings_menu.sizeHint().width(),
|
|
-1*self._settings_menu.sizeHint().height()
|
|
)
|
|
center = QtCore.QPoint(
|
|
int(self._settings_button.sizeHint().width()/2),
|
|
int(self._settings_button.sizeHint().height()/2)
|
|
)
|
|
where = self._settings_button.mapToGlobal(center+delta)
|
|
self._settings_menu.popup(where)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Refresh
|
|
#--------------------------------------------------------------------------
|
|
|
|
@disassembler.execute_ui
|
|
def refresh(self):
|
|
"""
|
|
Refresh the Coverage Overview.
|
|
"""
|
|
if not (self._table_model and self._shell and self._combobox):
|
|
return
|
|
|
|
self._table_model.refresh()
|
|
self._shell.refresh()
|
|
self._combobox.refresh()
|
|
|
|
@disassembler.execute_ui
|
|
def refresh_theme(self):
|
|
"""
|
|
Update visual elements based on theme change.
|
|
"""
|
|
if not (self._table_view and self._table_model and self._shell and self._combobox):
|
|
return
|
|
|
|
self._table_view.refresh_theme()
|
|
self._table_model.refresh_theme()
|
|
self._shell.refresh_theme()
|
|
self._combobox.refresh_theme()
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Qt Event Filter
|
|
#------------------------------------------------------------------------------
|
|
|
|
debugger_docked = False
|
|
|
|
class EventProxy(QtCore.QObject):
|
|
|
|
#
|
|
# NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7? Just gonna ship our own...
|
|
# - https://doc.qt.io/qt-5/qevent.html#Type-enum
|
|
#
|
|
|
|
EventShow = 17
|
|
EventDestroy = 16
|
|
EventLayoutRequest = 76
|
|
EventUpdateLater = 78
|
|
|
|
def __init__(self, target):
|
|
super(EventProxy, self).__init__()
|
|
self._target = weakref.proxy(target)
|
|
self._first_hit = True
|
|
|
|
def eventFilter(self, source, event):
|
|
|
|
#
|
|
# hook the destroy event of the coverage overview widget so that we can
|
|
# cleanup after ourselves in the interest of stability
|
|
#
|
|
|
|
if int(event.type()) == self.EventDestroy:
|
|
source.removeEventFilter(self)
|
|
|
|
#
|
|
# XXX/V35: This is pretty hacky annoying stuff, but the lifetime
|
|
# of the CoverageOverview widget is managed internally by binja
|
|
# and gets deleted/cleaned up *after* a database is closed.
|
|
#
|
|
# it's best we just unload the lighthouse context in binja after
|
|
# the UI widgets have been destroyed (which aligns with IDA)
|
|
#
|
|
|
|
if disassembler.NAME == "BINJA":
|
|
lctx = self._target.lctx
|
|
core = lctx.core
|
|
core.binja_close_context(lctx.dctx)
|
|
|
|
# cleanup the UI / qt references for the CoverageOverview elements
|
|
self._target.terminate()
|
|
|
|
#
|
|
# this seems to be 'roughly' the last event triggered after the widget
|
|
# is done initializing in both IDA and Binja, but prior to the first
|
|
# user-triggered 'show' events.
|
|
#
|
|
# this is mostly to account for the fact that binja 'shows' the widget
|
|
# when it is initially created (outside of our control). this was
|
|
# causing lighthouse to automatically cache database metadata when
|
|
# every database was opened ...
|
|
#
|
|
|
|
elif int(event.type()) == self.EventLayoutRequest:
|
|
self._target.initialized = True
|
|
|
|
#
|
|
# this is used to hook a little bit after the 'show' event of the
|
|
# coverage overview. this is the most universal signal that the
|
|
# user is *actually* trying to use lighthouse in a meaningful way...
|
|
#
|
|
# we will use this moment first to check if they skipped straight to
|
|
# 'go' and opened the coverage overview without the metadata cache
|
|
# getting built.
|
|
#
|
|
# this case should only happen if the user does 'Show Coverage
|
|
# Overview' from the binja-controlled Window menu entry...
|
|
#
|
|
|
|
elif int(event.type()) == self.EventUpdateLater:
|
|
|
|
if self._target.visible and self._first_hit:
|
|
self._first_hit = False
|
|
|
|
if disassembler.NAME == "BINJA":
|
|
self._target.lctx.start()
|
|
|
|
if not self._target.director.metadata.cached:
|
|
self._target.director.refresh()
|
|
|
|
#
|
|
# this is an unknown event, but it seems to fire when the widget is
|
|
# being saved/restored by a QMainWidget (in IDA). we use this to try
|
|
# and ensure the Coverage Overview stays docked when flipping between
|
|
# Reversing and Debugging states in IDA.
|
|
#
|
|
# See issue #16 on github for more information.
|
|
#
|
|
|
|
elif int(event.type()) == 2002 and disassembler.NAME == "IDA":
|
|
import idaapi
|
|
|
|
#
|
|
# if the general registers IDA View exists, we make the assumption
|
|
# that the user has probably started debugging.
|
|
#
|
|
|
|
debug_mode = bool(idaapi.find_widget("General registers"))
|
|
|
|
#
|
|
# if this is the first time the user has started debugging, dock
|
|
# the coverage overview in the debug QMainWidget workspace. its
|
|
# dock status / position should persist future debugger launches.
|
|
#
|
|
|
|
global debugger_docked
|
|
if debug_mode and not debugger_docked:
|
|
idaapi.set_dock_pos(self._target._title, "Structures", idaapi.DP_TAB)
|
|
debugger_docked = True
|
|
|
|
return False
|