mirror of
https://github.com/gaasedelen/lighthouse
synced 2026-06-08 14:19:10 +00:00
1452 lines
50 KiB
Python
1452 lines
50 KiB
Python
import os
|
|
import time
|
|
import string
|
|
import logging
|
|
from operator import itemgetter, attrgetter
|
|
|
|
from lighthouse.util import lmsg
|
|
from lighthouse.util.qt import *
|
|
from lighthouse.util.python import *
|
|
from lighthouse.util.misc import mainthread
|
|
from lighthouse.util.disassembler import disassembler
|
|
from lighthouse.coverage import FunctionCoverage, BADADDR
|
|
|
|
logger = logging.getLogger("Lighthouse.UI.Table")
|
|
|
|
#------------------------------------------------------------------------------
|
|
# CoverageTableView
|
|
#------------------------------------------------------------------------------
|
|
|
|
class CoverageTableView(QtWidgets.QTableView):
|
|
"""
|
|
The Coverage Table View (UI)
|
|
"""
|
|
|
|
def __init__(self, controller, model, parent=None):
|
|
super(CoverageTableView, self).__init__(parent)
|
|
self.setObjectName(self.__class__.__name__)
|
|
|
|
# underlying table controller object (MVC)
|
|
self._controller = controller
|
|
|
|
# underlying data model for the coverage table
|
|
self._model = model
|
|
self.setModel(self._model)
|
|
|
|
# configure the widget for use
|
|
self._ui_init()
|
|
self.refresh_theme()
|
|
|
|
@disassembler.execute_ui
|
|
def refresh_theme(self):
|
|
"""
|
|
Refresh UI facing elements to reflect the current theme.
|
|
"""
|
|
palette = self._model.lctx.palette
|
|
self.setStyleSheet(
|
|
"QTableView {"
|
|
" gridline-color: %s;" % palette.table_grid.name() +
|
|
" background-color: %s;" % palette.table_background.name() +
|
|
" color: %s;" % palette.table_text.name() +
|
|
" outline: none; "
|
|
"} " +
|
|
"QHeaderView::section { "
|
|
" padding: 1ex;" \
|
|
" margin: 0;" \
|
|
"} " +
|
|
"QTableView::item:selected {"
|
|
" color: white; "
|
|
" background-color: %s;" % palette.table_selection.name() +
|
|
"}"
|
|
)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# QTableView Overloads
|
|
#--------------------------------------------------------------------------
|
|
|
|
def keyPressEvent(self, event):
|
|
"""
|
|
Overload QTableView key press events.
|
|
"""
|
|
|
|
# remap h/j/k/l to arrow keys (VIM bindings)
|
|
if event.key() == QtCore.Qt.Key_J:
|
|
event = remap_key_event(event, QtCore.Qt.Key_Down)
|
|
elif event.key() == QtCore.Qt.Key_K:
|
|
event = remap_key_event(event, QtCore.Qt.Key_Up)
|
|
elif event.key() == QtCore.Qt.Key_H:
|
|
event = remap_key_event(event, QtCore.Qt.Key_Left)
|
|
elif event.key() == QtCore.Qt.Key_L:
|
|
event = remap_key_event(event, QtCore.Qt.Key_Right)
|
|
|
|
# handle the keypress as normal
|
|
super(CoverageTableView, self).keyPressEvent(event)
|
|
|
|
#
|
|
# after handling the keypress, immediately repaint the table. we use
|
|
# this to try to cut down on flicker / row skipping while scrolling
|
|
# using the keypad
|
|
#
|
|
|
|
self.repaint()
|
|
flush_qt_events()
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Initialization - UI
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _ui_init(self):
|
|
"""
|
|
Initialize UI elements.
|
|
"""
|
|
self._ui_init_table()
|
|
self._ui_init_table_ctx_menu_actions()
|
|
self._ui_init_header_ctx_menu_actions()
|
|
self._ui_init_signals()
|
|
|
|
def _ui_init_table(self):
|
|
"""
|
|
Initialize the coverage table.
|
|
"""
|
|
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
|
self.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
|
|
|
|
# these properties will allow the user shrink the table to any size
|
|
self.setMinimumHeight(0)
|
|
self.setSizePolicy(
|
|
QtWidgets.QSizePolicy.Ignored,
|
|
QtWidgets.QSizePolicy.Ignored
|
|
)
|
|
|
|
#
|
|
# Column Width
|
|
#
|
|
|
|
# get the font used by the table headers
|
|
title_font = self._model.headerData(0, QtCore.Qt.Horizontal, QtCore.Qt.FontRole)
|
|
title_fm = QtGui.QFontMetricsF(title_font)
|
|
|
|
# get the font used by the table cell entries
|
|
entry_font = self._model.data(0, QtCore.Qt.FontRole)
|
|
entry_fm = QtGui.QFontMetricsF(entry_font)
|
|
|
|
# get the font used by the table cell entries
|
|
entry_font = self._model.data(0, QtCore.Qt.FontRole)
|
|
entry_fm = QtGui.QFontMetricsF(entry_font)
|
|
|
|
# set the initial column widths based on their title or contents
|
|
for i in xrange(self._model.columnCount()):
|
|
|
|
# determine the pixel width of the column header text
|
|
title_rect = self._model.headerData(i, QtCore.Qt.Horizontal, QtCore.Qt.SizeHintRole)
|
|
|
|
# determine the pixel width of sample column entry text
|
|
entry_text = self._model.SAMPLE_CONTENTS[i]
|
|
entry_rect = entry_fm.boundingRect(entry_text)
|
|
|
|
# select the larger of the two potential column widths
|
|
column_width = int(max(title_rect.width(), entry_rect.width()*1.2))
|
|
|
|
# save the final column width
|
|
self.setColumnWidth(i, column_width)
|
|
|
|
#
|
|
# Misc
|
|
#
|
|
|
|
# clicking the table will select the entire row, not a single cell
|
|
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
|
|
|
# more code-friendly, readable aliases
|
|
vh = self.verticalHeader()
|
|
hh = self.horizontalHeader()
|
|
|
|
# hide the *vertical* (row) headers because we don't use them
|
|
vh.hide()
|
|
|
|
# stretch last table column (which is blank) to fill remaining space
|
|
#hh.setStretchLastSection(True)
|
|
#hh.setCascadingSectionResizes(True)
|
|
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
|
|
|
# disable bolding of table column headers when table is selected
|
|
hh.setHighlightSections(False)
|
|
|
|
# allow sorting of the table by clicking table headers, and set the
|
|
# default table state to be sorted by function address
|
|
self.setSortingEnabled(True)
|
|
hh.setSortIndicator(
|
|
CoverageTableModel.FUNC_ADDR,
|
|
QtCore.Qt.AscendingOrder
|
|
)
|
|
|
|
#
|
|
# Row Height
|
|
#
|
|
|
|
# force the table row heights to be fixed height
|
|
vh.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
|
|
|
|
# specify the fixed pixel height for the table rows
|
|
# NOTE: don't ask too many questions about this voodoo math :D
|
|
spacing = entry_fm.height() - entry_fm.xHeight()
|
|
tweak = (17*get_dpi_scale() - spacing)/get_dpi_scale()
|
|
vh.setDefaultSectionSize(int(entry_fm.height()+tweak))
|
|
|
|
def _ui_init_table_ctx_menu_actions(self):
|
|
"""
|
|
Initialize the right click context menu actions for the table view.
|
|
"""
|
|
|
|
# misc actions
|
|
self._action_dump_orphan = QtWidgets.QAction("Dump orphan addresses", None)
|
|
self._action_dump_internal = QtWidgets.QAction("Dump internal addresses (Debug)", None)
|
|
|
|
# function actions
|
|
self._action_rename = QtWidgets.QAction("Rename", None)
|
|
self._action_copy_name = QtWidgets.QAction("Copy name", None)
|
|
self._action_copy_address = QtWidgets.QAction("Copy address", None)
|
|
self._action_copy_name_and_address = QtWidgets.QAction("Copy name and address", None)
|
|
|
|
self._action_copy_names = QtWidgets.QAction("Copy names", None)
|
|
self._action_copy_addresses = QtWidgets.QAction("Copy addresses", None)
|
|
self._action_copy_names_and_addresses = QtWidgets.QAction("Copy names and addresses", None)
|
|
|
|
# function prefixing actions
|
|
self._action_prefix = QtWidgets.QAction("Prefix selected functions", None)
|
|
self._action_clear_prefix = QtWidgets.QAction("Clear prefixes", None)
|
|
|
|
def _ui_init_header_ctx_menu_actions(self):
|
|
"""
|
|
Initialize the right click context menu actions for the table header.
|
|
"""
|
|
self._action_alignment = QtWidgets.QAction("Center Aligned", None)
|
|
self._action_alignment.setCheckable(True)
|
|
self._action_alignment.setChecked(True)
|
|
|
|
def _ui_init_signals(self):
|
|
"""
|
|
Connect UI signals.
|
|
"""
|
|
|
|
# jump to disassembly on table row double click
|
|
self.doubleClicked.connect(self._ui_entry_double_click)
|
|
|
|
# right click popup menu (table)
|
|
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
self.customContextMenuRequested.connect(self._ui_table_ctx_menu_handler)
|
|
|
|
# right click popup menu (table header)
|
|
hh = self.horizontalHeader()
|
|
hh.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
hh.customContextMenuRequested.connect(self._ui_header_ctx_menu_handler)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Signal Handlers
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _ui_entry_double_click(self, index):
|
|
"""
|
|
Handle double click event on the coverage table.
|
|
|
|
A double click on the coverage table view will jump the user to
|
|
the corresponding function in the IDA disassembly view.
|
|
"""
|
|
self._controller.navigate_to_function(index.row())
|
|
|
|
def _ui_table_ctx_menu_handler(self, position):
|
|
"""
|
|
Handle right click context menu event on the coverage table.
|
|
"""
|
|
|
|
# create a right click menu based on the state and context
|
|
ctx_menu = self._populate_table_ctx_menu()
|
|
if not ctx_menu:
|
|
return
|
|
|
|
# show the popup menu to the user, and wait for their selection
|
|
if USING_PYSIDE6:
|
|
exec_func = getattr(ctx_menu, "exec")
|
|
else:
|
|
exec_func = getattr(ctx_menu, "exec_")
|
|
|
|
action = exec_func(self.viewport().mapToGlobal(position))
|
|
|
|
# process the user action
|
|
self._process_table_ctx_menu_action(action)
|
|
|
|
def _ui_header_ctx_menu_handler(self, position):
|
|
"""
|
|
Handle right click context menu event on the coverage table header.
|
|
"""
|
|
hh = self.horizontalHeader()
|
|
|
|
# get the table column where the right-click occurred
|
|
column = hh.logicalIndexAt(position)
|
|
|
|
# create a right click menu based on the state and context
|
|
ctx_menu = self._populate_header_ctx_menu()
|
|
if not ctx_menu:
|
|
return
|
|
|
|
# show the popup menu to the user, and wait for their selection
|
|
if USING_PYSIDE6:
|
|
exec_func = getattr(ctx_menu, "exec")
|
|
else:
|
|
exec_func = getattr(ctx_menu, "exec_")
|
|
action = exec_func(hh.viewport().mapToGlobal(position))
|
|
|
|
# process the user action
|
|
self._process_header_ctx_menu_action(action, column)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Context Menu (Table Rows)
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _populate_table_ctx_menu(self):
|
|
"""
|
|
Populate a context menu for the table view based on selection.
|
|
|
|
Returns a populated QMenu, or None.
|
|
"""
|
|
|
|
# get the list rows currently selected in the coverage table
|
|
selected_row_indexes = self.selectionModel().selectedRows()
|
|
if len(selected_row_indexes) == 0:
|
|
return None
|
|
|
|
# the context menu we will dynamically populate
|
|
ctx_menu = QtWidgets.QMenu()
|
|
|
|
#
|
|
# if there is only one table row selected (a function entry), then
|
|
# show the menu actions available for a single function such as
|
|
# copy function name, address, or renaming the function.
|
|
#
|
|
|
|
if len(selected_row_indexes) == 1:
|
|
|
|
row = selected_row_indexes[0].row()
|
|
function_address = self._model.row2func[row]
|
|
|
|
# special handling for right click of orphan coverage row
|
|
if function_address == BADADDR:
|
|
ctx_menu.addAction(self._action_dump_orphan)
|
|
ctx_menu.addAction(self._action_dump_internal)
|
|
return ctx_menu
|
|
|
|
# normal right click of a function row
|
|
else:
|
|
ctx_menu.addAction(self._action_rename)
|
|
ctx_menu.addSeparator()
|
|
ctx_menu.addAction(self._action_copy_name)
|
|
ctx_menu.addAction(self._action_copy_address)
|
|
ctx_menu.addAction(self._action_copy_name_and_address)
|
|
ctx_menu.addSeparator()
|
|
|
|
#
|
|
# if multiple functions are selected then show actions available
|
|
# for multiple functions.
|
|
#
|
|
|
|
else:
|
|
ctx_menu.addAction(self._action_copy_names)
|
|
ctx_menu.addAction(self._action_copy_addresses)
|
|
ctx_menu.addAction(self._action_copy_names_and_addresses)
|
|
ctx_menu.addSeparator()
|
|
|
|
# function prefixing actions
|
|
ctx_menu.addAction(self._action_prefix)
|
|
ctx_menu.addAction(self._action_clear_prefix)
|
|
|
|
# return the completed context menu
|
|
return ctx_menu
|
|
|
|
def _process_table_ctx_menu_action(self, action):
|
|
"""
|
|
Process the given (user selected) table view context menu action.
|
|
"""
|
|
|
|
# a right click menu action was not clicked. nothing else to do
|
|
if not action:
|
|
return
|
|
|
|
# get the list rows currently selected in the coverage table
|
|
row_indexes = self.selectionModel().selectedRows()
|
|
rows = [index.row() for index in row_indexes]
|
|
if len(rows) == 0:
|
|
return
|
|
|
|
# handle the 'Rename' action (only applies to a single function)
|
|
if action == self._action_rename and len(rows) == 1:
|
|
self._controller.rename_table_function(rows[0])
|
|
|
|
# handle the 'Copy name' or 'Copy names' action
|
|
elif action in [self._action_copy_name, self._action_copy_names]:
|
|
self._controller.copy_name(rows)
|
|
|
|
# handle the 'Copy address' or 'Copy addresses' action
|
|
elif action in [self._action_copy_address, self._action_copy_addresses]:
|
|
self._controller.copy_address(rows)
|
|
|
|
# handle the 'Copy name and address' or 'Copy names and addresses' action
|
|
elif action in [self._action_copy_name_and_address, self._action_copy_names_and_addresses]:
|
|
self._controller.copy_name_and_address(rows)
|
|
|
|
# handle the 'Prefix functions' action
|
|
elif action == self._action_prefix:
|
|
self._controller.prefix_table_functions(rows)
|
|
|
|
# handle the 'Clear prefix' action
|
|
elif action == self._action_clear_prefix:
|
|
self._controller.clear_function_prefixes(rows)
|
|
|
|
# handle the 'Dump orphan addresses' action
|
|
elif action == self._action_dump_orphan:
|
|
self._controller.dump_orphan()
|
|
|
|
# handle the 'Dump internal addresses' action
|
|
elif action == self._action_dump_internal:
|
|
self._controller.dump_internal()
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Context Menu (Table Header)
|
|
#--------------------------------------------------------------------------
|
|
|
|
def _populate_header_ctx_menu(self):
|
|
"""
|
|
Populate a context menu for the table header.
|
|
|
|
Return a populated QMenu, or None.
|
|
"""
|
|
ctx_menu = QtWidgets.QMenu()
|
|
ctx_menu.addAction(self._action_alignment)
|
|
return ctx_menu
|
|
|
|
def _process_header_ctx_menu_action(self, action, column):
|
|
"""
|
|
Process the given (user selected) table header context menu action.
|
|
"""
|
|
|
|
# a right click menu action was not clicked. nothing else to do
|
|
if not action:
|
|
return
|
|
|
|
# handle the 'Center Aligned' toggle action
|
|
if action == self._action_alignment:
|
|
self._controller.toggle_column_alignment(column)
|
|
|
|
#------------------------------------------------------------------------------
|
|
# CoverageTableController
|
|
#------------------------------------------------------------------------------
|
|
|
|
class CoverageTableController(object):
|
|
"""
|
|
The Coverage Table Controller (Logic)
|
|
"""
|
|
|
|
def __init__(self, lctx, model):
|
|
self.lctx = lctx
|
|
self._model = model
|
|
self._last_directory = None
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Renaming
|
|
#---------------------------------------------------------------------------
|
|
|
|
@mainthread
|
|
def rename_table_function(self, row):
|
|
"""
|
|
Interactive rename of a database function via the coverage table.
|
|
"""
|
|
|
|
# retrieve details about the function targeted for rename
|
|
function_address = self._model.row2func[row]
|
|
if function_address == BADADDR:
|
|
return
|
|
|
|
original_name = disassembler[self.lctx].get_function_raw_name_at(function_address)
|
|
|
|
# prompt the user for a new function name
|
|
ok, new_name = prompt_string(
|
|
"Please enter function name",
|
|
"Rename Function",
|
|
original_name
|
|
)
|
|
|
|
#
|
|
# if the user clicked cancel, or the name they entered
|
|
# is identical to the original, there's nothing to do
|
|
#
|
|
|
|
if not (ok or new_name != original_name):
|
|
return
|
|
|
|
# rename the function
|
|
disassembler[self.lctx].set_function_name_at(function_address, new_name)
|
|
|
|
@mainthread
|
|
def prefix_table_functions(self, rows):
|
|
"""
|
|
Interactive prefixing of database functions via the coverage table.
|
|
"""
|
|
|
|
# prompt the user for a new function name
|
|
ok, prefix = prompt_string(
|
|
"Please enter a function prefix",
|
|
"Prefix Function(s)",
|
|
"MyPrefix"
|
|
)
|
|
|
|
# bail if the user clicked cancel or failed to enter a prefix
|
|
if not (ok and prefix):
|
|
return
|
|
|
|
# apply the user prefix to the functions depicted in the given rows
|
|
function_addresses = self._get_function_addresses(rows)
|
|
disassembler[self.lctx].prefix_functions(function_addresses, prefix)
|
|
|
|
@mainthread
|
|
def clear_function_prefixes(self, rows):
|
|
"""
|
|
Clear prefixes of database functions via the coverage table.
|
|
"""
|
|
function_addresses = self._get_function_addresses(rows)
|
|
disassembler[self.lctx].clear_prefixes(function_addresses)
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Copy-to-Clipboard
|
|
#---------------------------------------------------------------------------
|
|
|
|
@mainthread
|
|
def copy_name(self, rows):
|
|
"""
|
|
Copy function names for the given table rows to clipboard.
|
|
"""
|
|
model = self._model
|
|
function_names = ""
|
|
for row_number in rows:
|
|
name_index = model.index(row_number, model.FUNC_NAME)
|
|
function_names += model.data(name_index)
|
|
function_names += "\n"
|
|
copy_to_clipboard(function_names.rstrip())
|
|
return function_names
|
|
|
|
@mainthread
|
|
def copy_address(self, rows):
|
|
"""
|
|
Copy function addresses for the given table rows to clipboard.
|
|
"""
|
|
model = self._model
|
|
address_string = ""
|
|
for row_number in rows:
|
|
addr_index = model.index(row_number, model.FUNC_ADDR)
|
|
address_string += model.data(addr_index)
|
|
address_string += "\n"
|
|
copy_to_clipboard(address_string.rstrip())
|
|
return address_string
|
|
|
|
@mainthread
|
|
def copy_name_and_address(self, rows):
|
|
"""
|
|
Copy function name & addresses for the given table rows to clipboard.
|
|
"""
|
|
model = self._model
|
|
function_name_and_address = ""
|
|
for row_number in rows:
|
|
name_index = model.index(row_number, model.FUNC_NAME)
|
|
addr_index = model.index(row_number, model.FUNC_ADDR)
|
|
function_name_and_address += model.data(addr_index)
|
|
function_name_and_address += " "
|
|
function_name_and_address += model.data(name_index)
|
|
function_name_and_address += "\n"
|
|
copy_to_clipboard(function_name_and_address.rstrip())
|
|
return function_name_and_address
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Dumping
|
|
#--------------------------------------------------------------------------
|
|
|
|
def dump_orphan(self):
|
|
"""
|
|
Dump the orphan coverage data.
|
|
"""
|
|
coverage = self.lctx.director.coverage
|
|
lmsg("Orphan coverage addresses for %s:" % coverage.name)
|
|
self._dump_addresses(coverage.orphan_addresses)
|
|
|
|
def dump_internal(self):
|
|
"""
|
|
Dump the internal coverage data.
|
|
"""
|
|
coverage = self.lctx.director.coverage
|
|
lmsg("Internal coverage addresses for %s:" % coverage.name)
|
|
self._dump_addresses(coverage.unmapped_addresses)
|
|
|
|
def _dump_addresses(self, coverage_addresses):
|
|
"""
|
|
Dump the given list of addresses to the terminal.
|
|
"""
|
|
coverage_addresses = sorted(coverage_addresses)
|
|
if not coverage_addresses:
|
|
lmsg(" * (there is no addresses to dump)")
|
|
return
|
|
|
|
for address in coverage_addresses:
|
|
lmsg(" * 0x%X" % address)
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Misc
|
|
#---------------------------------------------------------------------------
|
|
|
|
def navigate_to_function(self, row):
|
|
"""
|
|
Navigate to the function depicted by the given row.
|
|
"""
|
|
|
|
# get the clicked function address
|
|
function_address = self._model.row2func[row]
|
|
if function_address == BADADDR:
|
|
return
|
|
|
|
#
|
|
# if there is actually coverage in the function, attempt to locate the
|
|
# first block (or any block) with coverage and set that as our target
|
|
#
|
|
|
|
function_coverage = self.lctx.director.coverage.functions.get(function_address, None)
|
|
if function_coverage:
|
|
if function_address in function_coverage.nodes:
|
|
target_address = function_address
|
|
else:
|
|
target_address = sorted(function_coverage.nodes)[0]
|
|
|
|
#
|
|
# if the user clicked a function with no coverage, we should just
|
|
# navigate to the top of the function... nothing fancy
|
|
#
|
|
|
|
else:
|
|
target_address = function_address
|
|
|
|
# navigate to the target function + block
|
|
disassembler[self.lctx].navigate_to_function(function_address, target_address)
|
|
|
|
def toggle_column_alignment(self, column):
|
|
"""
|
|
Toggle the text alignment of given column.
|
|
"""
|
|
index = self._model.index(0, column)
|
|
alignment = self._model.data(index, QtCore.Qt.TextAlignmentRole)
|
|
|
|
# toggle the column alignment between center (default) and left
|
|
if alignment == QtCore.Qt.AlignCenter:
|
|
new_alignment = QtCore.Qt.AlignVCenter
|
|
else:
|
|
new_alignment = QtCore.Qt.AlignCenter
|
|
|
|
# send the new alignment to the model
|
|
self._model.set_column_alignment(column, new_alignment)
|
|
|
|
def export_to_html(self):
|
|
"""
|
|
Export the coverage table to an HTML report.
|
|
"""
|
|
if not self._last_directory:
|
|
self._last_directory = disassembler[self.lctx].get_database_directory()
|
|
|
|
# build filename for the coverage report based off the coverage name
|
|
name, _ = os.path.splitext(self.lctx.director.coverage_name)
|
|
filename = name + ".html"
|
|
suggested_filepath = os.path.join(self._last_directory, filename)
|
|
|
|
# create & configure a Qt File Dialog for immediate use
|
|
file_dialog = QtWidgets.QFileDialog()
|
|
file_dialog.setFileMode(QtWidgets.QFileDialog.AnyFile)
|
|
|
|
# we construct kwargs here for cleaner PySide/PyQt5 compatibility
|
|
kwargs = \
|
|
{
|
|
"filter": "HTML Files (*.html)",
|
|
"caption": "Save HTML Report",
|
|
}
|
|
|
|
if USING_PYQT5:
|
|
kwargs["directory"] = suggested_filepath
|
|
else:
|
|
kwargs["dir"] = suggested_filepath
|
|
|
|
# prompt the user with the file dialog, and await their chosen filename(s)
|
|
filename, _ = file_dialog.getSaveFileName(**kwargs)
|
|
if not filename:
|
|
return
|
|
|
|
# remember the last directory we were in (parsed from the saved file)
|
|
self._last_directory = os.path.dirname(filename) + os.sep
|
|
|
|
# write the generated HTML report to disk
|
|
with open(filename, "w") as fd:
|
|
fd.write(self._model.to_html())
|
|
|
|
lmsg("Saved HTML report to %s" % filename)
|
|
|
|
#---------------------------------------------------------------------------
|
|
# Internal
|
|
#---------------------------------------------------------------------------
|
|
|
|
def _get_function_addresses(self, rows):
|
|
"""
|
|
Return the function addresses for the given rows.
|
|
"""
|
|
function_addresses = []
|
|
for row_number in rows:
|
|
address = self._model.row2func[row_number]
|
|
if address != BADADDR:
|
|
function_addresses.append(address)
|
|
return function_addresses
|
|
|
|
#------------------------------------------------------------------------------
|
|
# CoverageTableModel
|
|
#------------------------------------------------------------------------------
|
|
|
|
class CoverageTableModel(QtCore.QAbstractTableModel):
|
|
"""
|
|
A Qt model interface to format coverage data for Qt views.
|
|
"""
|
|
|
|
# named constants for coverage table column indexes
|
|
COV_PERCENT = 0
|
|
FUNC_NAME = 1
|
|
FUNC_ADDR = 2
|
|
BLOCKS_HIT = 3
|
|
INST_HIT = 4
|
|
FUNC_SIZE = 5
|
|
COMPLEXITY = 6
|
|
|
|
METADATA_ATTRIBUTES = [FUNC_NAME, FUNC_ADDR, FUNC_SIZE, COMPLEXITY]
|
|
COVERAGE_ATTRIBUTES = [COV_PERCENT, BLOCKS_HIT, INST_HIT]
|
|
|
|
# column index -> object attribute mapping
|
|
COLUMN_TO_FIELD = \
|
|
{
|
|
COV_PERCENT: "instruction_percent",
|
|
FUNC_NAME: "name",
|
|
FUNC_ADDR: "address",
|
|
BLOCKS_HIT: "nodes_executed",
|
|
INST_HIT: "instructions_executed",
|
|
FUNC_SIZE: "size",
|
|
COMPLEXITY: "cyclomatic_complexity"
|
|
}
|
|
|
|
# column headers of the table
|
|
COLUMN_HEADERS = \
|
|
{
|
|
COV_PERCENT: "Cov %",
|
|
FUNC_NAME: "Func Name",
|
|
FUNC_ADDR: "Address",
|
|
BLOCKS_HIT: "Blocks Hit",
|
|
INST_HIT: "Instr. Hit",
|
|
FUNC_SIZE: "Func Size",
|
|
COMPLEXITY: "CC",
|
|
}
|
|
|
|
# column header tooltips
|
|
COLUMN_TOOLTIPS = \
|
|
{
|
|
COV_PERCENT: "Coverage Percent",
|
|
FUNC_NAME: "Function Name",
|
|
FUNC_ADDR: "Function Address",
|
|
BLOCKS_HIT: "Number of Basic Blocks Executed",
|
|
INST_HIT: "Number of Instructions Executed",
|
|
FUNC_SIZE: "Function Size (bytes)",
|
|
COMPLEXITY: "Cyclomatic Complexity",
|
|
}
|
|
|
|
# sample column
|
|
SAMPLE_CONTENTS = \
|
|
[
|
|
" 100.00 ",
|
|
" sub_140001B20 ",
|
|
" 0x140001b20 ",
|
|
" 100 / 100 ",
|
|
" 1000 / 1000 ",
|
|
" 100000 ",
|
|
" 1000 ",
|
|
]
|
|
|
|
def __init__(self, lctx, parent=None):
|
|
super(CoverageTableModel, self).__init__(parent)
|
|
self.lctx = lctx
|
|
self._director = lctx.director
|
|
|
|
# convenience mapping from row_number --> function_address
|
|
self.row2func = {}
|
|
self._row_count = 0
|
|
|
|
# an internal mapping of the data / coverage to make visible
|
|
self._no_coverage = []
|
|
self._visible_metadata = {}
|
|
self._visible_coverage = {}
|
|
|
|
# a fallback coverage object for functions with no coverage
|
|
self._blank_coverage = FunctionCoverage(BADADDR)
|
|
self._blank_coverage.coverage_color = lctx.palette.table_coverage_none
|
|
|
|
# set the default column text alignment for each column (centered)
|
|
self._default_alignment = QtCore.Qt.AlignCenter
|
|
self._column_alignment = [
|
|
self._default_alignment for x in self.COLUMN_HEADERS
|
|
]
|
|
|
|
# make the function name column left aligned by default
|
|
self.set_column_alignment(self.FUNC_NAME, QtCore.Qt.AlignVCenter)
|
|
|
|
# initialize a monospace font to use for table row / cell text
|
|
self._entry_font = MonospaceFont()
|
|
if not USING_PYSIDE6:
|
|
#TODO Figure out if this matters?
|
|
self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics)
|
|
self._entry_font.setPointSizeF(normalize_to_dpi(10))
|
|
|
|
# use the default / system font for the column titles
|
|
self._title_font = QtGui.QFont()
|
|
self._title_font.setPointSizeF(normalize_to_dpi(10))
|
|
|
|
#----------------------------------------------------------------------
|
|
# Sorting
|
|
#----------------------------------------------------------------------
|
|
|
|
# attributes to track the model's last known (column) sort state
|
|
self._last_sort = self.FUNC_ADDR
|
|
self._last_sort_order = QtCore.Qt.AscendingOrder
|
|
|
|
#----------------------------------------------------------------------
|
|
# Filters
|
|
#----------------------------------------------------------------------
|
|
|
|
# OPTION: display 0% coverage entries
|
|
self._hide_zero = False
|
|
|
|
# OPTION: display functions matching search_string (substring)
|
|
self._search_string = ""
|
|
|
|
#----------------------------------------------------------------------
|
|
# Signals
|
|
#----------------------------------------------------------------------
|
|
|
|
# register for cues from the director
|
|
self._director.coverage_switched(self._internal_refresh)
|
|
self._director.coverage_modified(self._internal_refresh)
|
|
self._director.metadata.function_renamed(self._data_changed)
|
|
|
|
def refresh_theme(self):
|
|
"""
|
|
Refresh UI facing elements to reflect the current theme.
|
|
|
|
Does not require @disassembler.execute_ui decorator, data_changed() has its own.
|
|
"""
|
|
self._blank_coverage.coverage_color = self.lctx.palette.table_coverage_none
|
|
self._data_changed()
|
|
|
|
#--------------------------------------------------------------------------
|
|
# QAbstractTableModel Overloads
|
|
#--------------------------------------------------------------------------
|
|
|
|
def flags(self, index):
|
|
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
|
|
|
def rowCount(self, index=QtCore.QModelIndex()):
|
|
"""
|
|
The number of table rows.
|
|
"""
|
|
return self._row_count
|
|
|
|
def columnCount(self, index=QtCore.QModelIndex()):
|
|
"""
|
|
The number of table columns.
|
|
"""
|
|
return len(self.COLUMN_HEADERS)
|
|
|
|
def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
|
|
"""
|
|
Define the properties of the table rows & columns.
|
|
"""
|
|
|
|
if orientation == QtCore.Qt.Horizontal:
|
|
|
|
# the title of the header columns has been requested
|
|
if role == QtCore.Qt.DisplayRole:
|
|
return self.COLUMN_HEADERS[column]
|
|
|
|
# the text alignment of the header has been requested
|
|
elif role == QtCore.Qt.TextAlignmentRole:
|
|
|
|
# center align all columns
|
|
return self._column_alignment[column]
|
|
|
|
# tooltip request
|
|
elif role == QtCore.Qt.ToolTipRole:
|
|
return self.COLUMN_TOOLTIPS[column]
|
|
|
|
# font format request
|
|
elif role == QtCore.Qt.FontRole:
|
|
return self._title_font
|
|
|
|
if role == QtCore.Qt.SizeHintRole:
|
|
title_fm = QtGui.QFontMetricsF(self._title_font)
|
|
title_rect = title_fm.boundingRect(self.COLUMN_HEADERS[column])
|
|
padded = QtCore.QSize(int(title_rect.width()*1.45), int(title_rect.height()*1.75))
|
|
return padded
|
|
|
|
# unhandeled header request
|
|
return None
|
|
|
|
def _data_function_display(self, function_address, column):
|
|
"""
|
|
Return a string to diplay in the requested column of the given function.
|
|
"""
|
|
|
|
# lookup the function info for the given function
|
|
try:
|
|
function_metadata = self.lctx.metadata.functions[function_address]
|
|
|
|
#
|
|
# if we hit a KeyError, it is probably because the database metadata
|
|
# is being refreshed and the model (this object) has yet to be
|
|
# updated.
|
|
#
|
|
# this should only ever happen as a result of the user using the
|
|
# right click 'Refresh metadata' action. And even then, only when
|
|
# a function they undefined in the IDB is visible in the coverage
|
|
# overview table view.
|
|
#
|
|
# In theory, the table should get refreshed *after* the metadata
|
|
# refresh completes. So for now, we simply return return the filler
|
|
# string '?'
|
|
#
|
|
|
|
except KeyError:
|
|
return "?"
|
|
|
|
#
|
|
# remember, if a function does *not* have coverage data, it will
|
|
# not have an entry in the coverage map. that means we should
|
|
# yield a default, 'blank', coverage item in these instances
|
|
#
|
|
|
|
function_coverage = self._director.coverage.functions.get(
|
|
function_address,
|
|
self._blank_coverage
|
|
)
|
|
|
|
# Coverage % - (by instruction execution)
|
|
if column == self.COV_PERCENT:
|
|
return "%5.2f" % (function_coverage.instruction_percent*100)
|
|
|
|
# Function Name
|
|
elif column == self.FUNC_NAME:
|
|
return function_metadata.name
|
|
|
|
# Function Address
|
|
elif column == self.FUNC_ADDR:
|
|
return "0x%X" % function_metadata.address
|
|
|
|
# Basic Blocks
|
|
elif column == self.BLOCKS_HIT:
|
|
return "%3u / %-3u" % (function_coverage.nodes_executed,
|
|
function_metadata.node_count)
|
|
|
|
# Instructions Hit
|
|
elif column == self.INST_HIT:
|
|
return "%4u / %-4u" % (function_coverage.instructions_executed,
|
|
function_metadata.instruction_count)
|
|
|
|
# Function Size
|
|
elif column == self.FUNC_SIZE:
|
|
return "%u" % function_metadata.size
|
|
|
|
# Cyclomatic Complexity
|
|
elif column == self.COMPLEXITY:
|
|
return "%u" % function_metadata.cyclomatic_complexity
|
|
|
|
# unhandeled? maybe make this an assert?
|
|
return None
|
|
|
|
def _data_orphan_display(self, column):
|
|
"""
|
|
Return a string to be displayed by the table
|
|
"""
|
|
if column == self.FUNC_NAME:
|
|
return "Orphan Coverage"
|
|
elif column == self.INST_HIT:
|
|
return "%u" % len(self._director.coverage.orphan_addresses)
|
|
return "N/A"
|
|
|
|
def data(self, index, role=QtCore.Qt.DisplayRole):
|
|
"""
|
|
Define how Qt should access the underlying model data.
|
|
"""
|
|
|
|
# a request has been made for what text to show in a table cell
|
|
if role == QtCore.Qt.DisplayRole:
|
|
|
|
column = index.column()
|
|
function_address = self.row2func[index.row()]
|
|
|
|
if function_address == BADADDR:
|
|
return self._data_orphan_display(column)
|
|
else:
|
|
return self._data_function_display(function_address, column)
|
|
|
|
# cell background color request
|
|
elif role == QtCore.Qt.BackgroundRole:
|
|
function_address = self.row2func[index.row()]
|
|
|
|
# special handling for 'orphan' coverage
|
|
if function_address == BADADDR:
|
|
|
|
# if there was *ANY* coverage, color the 'orphan' line red
|
|
if self._director.coverage.orphan_addresses:
|
|
return self.lctx.palette.table_coverage_bad
|
|
|
|
# normal handling
|
|
function_coverage = self._director.coverage.functions.get(
|
|
function_address,
|
|
self._blank_coverage
|
|
)
|
|
|
|
return function_coverage.coverage_color
|
|
|
|
# cell font style format request
|
|
elif role == QtCore.Qt.FontRole:
|
|
return self._entry_font
|
|
|
|
# cell text alignment request
|
|
elif role == QtCore.Qt.TextAlignmentRole:
|
|
return self._column_alignment[index.column()]
|
|
|
|
# unhandeled request, nothing to do
|
|
return None
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Sorting
|
|
#--------------------------------------------------------------------------
|
|
|
|
def sort(self, column, sort_order):
|
|
"""
|
|
Sort the coverage table rows by the selected column, and direction.
|
|
"""
|
|
|
|
#
|
|
# look up the name of field in the FunctionCoverage class object
|
|
# that we would like to sort by based on the selected column
|
|
#
|
|
|
|
try:
|
|
sort_field = self.COLUMN_TO_FIELD[column]
|
|
|
|
# column has not been enlightened to sorting
|
|
except KeyError as e:
|
|
logger.error("ERROR: Sorting not implemented for column %u" % column)
|
|
self.layoutChanged.emit()
|
|
return
|
|
|
|
#
|
|
# In PySide6 (eg. Binary Ninja) the Qt.SortOrder type does not convert
|
|
# to a simple integer (unlike PyQt5) so we reduce the type for comapt
|
|
#
|
|
|
|
direction = (sort_order == QtCore.Qt.SortOrder.DescendingOrder)
|
|
|
|
#
|
|
# NOTE: attrgetter appears to profile ~8-12% faster than lambdas
|
|
# accessing the member on the member, hence the strange paradigm
|
|
#
|
|
|
|
# sort the table entries by a function metadata attribute
|
|
if column in self.METADATA_ATTRIBUTES:
|
|
sorted_functions = sorted(
|
|
itervalues(self._visible_metadata),
|
|
key=attrgetter(sort_field),
|
|
reverse=direction
|
|
)
|
|
|
|
# sort the table entries by a function coverage attribute
|
|
elif column in self.COVERAGE_ATTRIBUTES:
|
|
sorted_functions = sorted(
|
|
itervalues(self._visible_coverage),
|
|
key=attrgetter(sort_field),
|
|
reverse=direction
|
|
)
|
|
|
|
#
|
|
# we sorted only the functions items that have known coverage.
|
|
# but since some functions may not have had coverage, they were
|
|
# not included in the sort.
|
|
#
|
|
# we simply append (or prepend) these unsortable (no coverage)
|
|
# functions to the sorted list as they are still members of
|
|
# the visible set regardless of their coverage status
|
|
#
|
|
|
|
#
|
|
# if the sort was descending (100% --> 0%), the no_coverage
|
|
# items (0%) should be appended to the *end*
|
|
#
|
|
|
|
if direction:
|
|
sorted_functions += self._no_coverage
|
|
|
|
#
|
|
# if the sort was ascending (0% --> 100%), the no_coverage
|
|
# items (0%) should be prepended to the *front*
|
|
#
|
|
|
|
else:
|
|
sorted_functions = self._no_coverage + sorted_functions
|
|
|
|
# create a generator of the sorted function addresses
|
|
sorted_addresses = (x.address for x in sorted_functions)
|
|
|
|
# finally, rebuild the row2func mapping and notify views of this change
|
|
self.row2func = dict(zip(xrange(len(sorted_functions)), sorted_addresses))
|
|
self.row2func[len(self.row2func)] = BADADDR
|
|
self.func2row = {v: k for k, v in iteritems(self.row2func)}
|
|
self.layoutChanged.emit()
|
|
|
|
# save the details of this sort event as they may be needed later
|
|
self._last_sort = column
|
|
self._last_sort_order = sort_order
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Public
|
|
#--------------------------------------------------------------------------
|
|
|
|
def set_column_alignment(self, column, alignment):
|
|
"""
|
|
Set the text alignment of the given column.
|
|
"""
|
|
self._column_alignment[column] = alignment
|
|
|
|
# redraw the column header & row contents with the new alignment
|
|
self._alignment_changed(column)
|
|
|
|
def get_modeled_coverage_percent(self):
|
|
"""
|
|
Get the coverage % represented by the current (visible) model.
|
|
"""
|
|
|
|
# sum the # of instructions in all the visible functions
|
|
instruction_count = sum(
|
|
meta.instruction_count for meta in itervalues(self._visible_metadata)
|
|
)
|
|
|
|
# sum the # of instructions executed in all the visible functions
|
|
instructions_executed = sum(
|
|
cov.instructions_executed for cov in itervalues(self._visible_coverage)
|
|
)
|
|
|
|
# compute coverage percentage of the visible functions
|
|
return (float(instructions_executed) / (instruction_count or 1))*100
|
|
|
|
#--------------------------------------------------------------------------
|
|
# HTML Export
|
|
#--------------------------------------------------------------------------
|
|
|
|
def to_html(self):
|
|
"""
|
|
Generate an HTML representation of the coverage table.
|
|
"""
|
|
palette = self.lctx.palette
|
|
|
|
# table summary
|
|
summary_html, summary_css = self._generate_html_summary()
|
|
|
|
# coverage table
|
|
table_html, table_css = self._generate_html_table()
|
|
|
|
# page body
|
|
body_elements = [summary_html, table_html]
|
|
body_html = "<body>%s</body>" % '\n'.join(body_elements)
|
|
body_css = \
|
|
"""
|
|
body {{
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
|
|
color: {page_fg};
|
|
background-color: {page_bg};
|
|
}}
|
|
""".format(
|
|
page_fg=palette.table_text.name(),
|
|
page_bg=palette.html_page_background.name()
|
|
)
|
|
|
|
# HTML <head> tag
|
|
css_elements = [body_css, summary_css, table_css]
|
|
head_contents = "<style>%s</style>" % '\n'.join(css_elements)
|
|
head_html = "<head>%s</head>" % head_contents
|
|
|
|
# generate the final HTML page
|
|
page_elements = [head_html, body_html]
|
|
page_html = "<html>%s</html>" % '\n'.join(page_elements)
|
|
|
|
# return the generated HTML report
|
|
return page_html
|
|
|
|
def _generate_html_summary(self):
|
|
"""
|
|
Generate the HTML table summary.
|
|
"""
|
|
palette = self.lctx.palette
|
|
metadata = self._director.metadata
|
|
coverage = self._director.coverage
|
|
|
|
# page title
|
|
title_html = "<h1>Lighthouse Coverage Report</h1>"
|
|
|
|
# summary details
|
|
detail = lambda x,y: '<li><span class="detail">%s:</span> %s</li>' % (x,y)
|
|
database_percent = coverage.instruction_percent*100
|
|
table_percent = self.get_modeled_coverage_percent()
|
|
details = \
|
|
[
|
|
detail("Target Binary", metadata.filename),
|
|
detail("Coverage Name", coverage.name),
|
|
detail("Coverage File", coverage.filepath),
|
|
detail("Database Coverage", "%1.2f%%" % database_percent),
|
|
detail("Table Coverage", "%1.2f%%" % table_percent),
|
|
detail("Timestamp", time.ctime()),
|
|
]
|
|
list_html = "<ul>%s</ul>" % '\n'.join(details)
|
|
list_css = \
|
|
"""
|
|
.detail {{
|
|
font-weight: bold;
|
|
color: {page_fg};
|
|
}}
|
|
li {{
|
|
color: {detail_fg};
|
|
}}
|
|
""".format(
|
|
page_fg=palette.table_text.name(),
|
|
detail_fg=palette.html_summary_text.name()
|
|
)
|
|
|
|
# title + summary
|
|
summary_html = title_html + list_html
|
|
summary_css = list_css
|
|
return (summary_html, summary_css)
|
|
|
|
def _generate_html_table(self):
|
|
"""
|
|
Generate the HTML coverage table.
|
|
"""
|
|
palette = self.lctx.palette
|
|
table_rows = []
|
|
|
|
# generate the table's column title row
|
|
header_cells = []
|
|
for i in xrange(self.columnCount()-1):
|
|
header_cells.append(
|
|
"<th>%s</th>" % self.headerData(i, QtCore.Qt.Horizontal)
|
|
)
|
|
table_rows.append((palette.html_table_header.name(), header_cells))
|
|
|
|
# generate the table's coverage rows
|
|
for row in xrange(self.rowCount()):
|
|
row_cells = []
|
|
for column in xrange(self.columnCount()-1):
|
|
index = self.index(row, column)
|
|
row_cells.append("<td>%s</td>" % self.data(index))
|
|
row_color = self.data(index, QtCore.Qt.BackgroundRole).name()
|
|
table_rows.append((row_color, row_cells))
|
|
|
|
# wrap each row of cells, into an HTML table row
|
|
html_rows = []
|
|
for row_color, row_cells in table_rows:
|
|
cell_html = ''.join(row_cells)
|
|
html_rows.append("<tr style='background-color: %s'>%s</tr>" % (row_color, cell_html))
|
|
|
|
# generate the final HTML table
|
|
table_html = "<table>%s</table>" % '\n'.join(html_rows)
|
|
table_css = \
|
|
"""
|
|
table {{
|
|
text-align: center;
|
|
white-space: pre;
|
|
border-collapse: collapse;
|
|
|
|
background-color: {table_bg};
|
|
color: {table_fg};
|
|
}}
|
|
|
|
table, th, td {{
|
|
border: 1px solid black;
|
|
}}
|
|
|
|
table tr td:nth-child(2) {{
|
|
text-align: left;
|
|
}}
|
|
|
|
td {{
|
|
font-family: "Courier New", Courier, monospace;
|
|
font-size: 11pt;
|
|
padding: 0.5ex 1ex 0.5ex 1ex;
|
|
}}
|
|
|
|
th {{
|
|
padding: 1ex 1em 1ex 1em;
|
|
}}
|
|
""".format(
|
|
table_bg=palette.table_background.name(),
|
|
table_fg=palette.table_text.name()
|
|
)
|
|
|
|
return (table_html, table_css)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Filters
|
|
#--------------------------------------------------------------------------
|
|
|
|
def filter_zero_coverage(self, hide):
|
|
"""
|
|
Filter out zero coverage functions from the model.
|
|
"""
|
|
|
|
# the hide/unhide request matches the current state, ignore
|
|
if self._hide_zero == hide:
|
|
return
|
|
|
|
# the filter is changing states, so we need to recompute the model
|
|
self._hide_zero = hide
|
|
self._internal_refresh()
|
|
|
|
def filter_string(self, search_string):
|
|
"""
|
|
Filter out functions whose names do not contain the given substring.
|
|
"""
|
|
|
|
# the filter string matches the current string, ignore
|
|
if search_string == self._search_string:
|
|
return
|
|
|
|
# the filter is changing states, so we need to recompute the model
|
|
self._search_string = search_string
|
|
self._internal_refresh()
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Refresh
|
|
#--------------------------------------------------------------------------
|
|
|
|
def refresh(self):
|
|
"""
|
|
Public refresh of the coverage model.
|
|
"""
|
|
self._internal_refresh()
|
|
|
|
@disassembler.execute_ui
|
|
def _internal_refresh(self):
|
|
"""
|
|
Internal refresh of the coverage model.
|
|
"""
|
|
self._refresh_data()
|
|
|
|
# sort the data set according to the last selected sorted column
|
|
self.sort(self._last_sort, self._last_sort_order)
|
|
|
|
@mainthread
|
|
def _refresh_data(self):
|
|
"""
|
|
Initialize the mapping to go from displayed row to function.
|
|
"""
|
|
row = 0
|
|
self.row2func = {}
|
|
self.func2row = {}
|
|
self._row_count = 0
|
|
self._no_coverage = []
|
|
self._visible_coverage = {}
|
|
self._visible_metadata = {}
|
|
|
|
metadata = self._director.metadata
|
|
coverage = self._director.coverage
|
|
|
|
#
|
|
# if the search string is all lowercase, then we are going to perform
|
|
# a case insensitive search/filter.
|
|
#
|
|
# that means we want to 'normalize' all the function names by making
|
|
# them lowercase before searching for our needle (search str)
|
|
#
|
|
|
|
normalize = lambda x: x
|
|
if not (set(self._search_string) & set(string.ascii_uppercase)):
|
|
normalize = lambda x: x.lower()
|
|
|
|
#
|
|
# it's time to rebuild the list of coverage items to make visible in
|
|
# the coverage overview list. during this process, we filter out entries
|
|
# that do not meet the criteria as specified by the user.
|
|
#
|
|
|
|
# loop through *all* the functions as defined in the active metadata
|
|
for function_address in metadata.functions:
|
|
|
|
#------------------------------------------------------------------
|
|
# Filters - START
|
|
#------------------------------------------------------------------
|
|
|
|
# OPTION: ignore items with 0% coverage items
|
|
if self._hide_zero and not function_address in coverage.functions:
|
|
continue
|
|
|
|
# OPTIONS: ignore items that do not match the search string
|
|
if not self._search_string in normalize(metadata.functions[function_address].name):
|
|
continue
|
|
|
|
#------------------------------------------------------------------
|
|
# Filters - END
|
|
#------------------------------------------------------------------
|
|
|
|
# store a reference to the listed function's metadata
|
|
self._visible_metadata[function_address] = metadata.functions[function_address]
|
|
|
|
# store a reference to the listed function's coverage
|
|
if function_address in coverage.functions:
|
|
self._visible_coverage[function_address] = coverage.functions[function_address]
|
|
|
|
# reminder: coverage is *not* guaranteed :-)
|
|
else:
|
|
self._no_coverage.append(metadata.functions[function_address])
|
|
|
|
# map the function address to a visible row # for easy lookup
|
|
self.row2func[row] = function_address
|
|
row += 1
|
|
|
|
# add a special entry for 'orphan coverage'
|
|
self.row2func[len(self.row2func)] = BADADDR
|
|
|
|
# build the inverse func --> row mapping
|
|
self.func2row = {v: k for k, v in iteritems(self.row2func)}
|
|
|
|
# bake the final number of rows into the model
|
|
self._row_count = len(self.row2func)
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Qt Notifications
|
|
#--------------------------------------------------------------------------
|
|
|
|
@disassembler.execute_ui
|
|
def _data_changed(self):
|
|
"""
|
|
Notify attached views that simple model data has been updated/modified.
|
|
"""
|
|
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
|
|
|
|
@disassembler.execute_ui
|
|
def _alignment_changed(self, column):
|
|
"""
|
|
Notify attached views that the column alignment has been changed.
|
|
"""
|
|
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
|
|
self.headerDataChanged.emit(QtCore.Qt.Horizontal, column, column)
|