Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0350b0c538 | |||
| 88051b0652 | |||
| 1d7e4b321c | |||
| 602bb611a4 | |||
| c5cc03d7dc | |||
| 720b036a9d | |||
| b8ff268c86 | |||
| 88ceac7b19 | |||
| 9a56463103 | |||
| 562595be9b | |||
| eeb16a5f81 | |||
| 84eeb210c6 | |||
| f944b62cc3 | |||
| 1b883377d6 | |||
| 146eb4fd2d | |||
| de2704b680 | |||
| 96df2c5a1f | |||
| 9c579c6e9d | |||
| ee7ac30f14 | |||
| 5f8fa02de2 | |||
| 87defff5cd | |||
| e1438159da | |||
| 856768d129 | |||
| 7d67e41773 | |||
| 8f3c23bbb6 | |||
| 4992d4b731 | |||
| 550c476e81 | |||
| bba8d91253 | |||
| 0e0e08e564 | |||
| 6912018b89 | |||
| f4642e8b4b | |||
| 6e1dcb8d40 | |||
| 973b40e6d0 | |||
| e3d636a3f4 |
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2021 Markus Gaasedelen
|
||||
Copyright (c) 2017-2024 Markus Gaasedelen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -39,22 +39,13 @@ Use the instructions below for your respective disassembler.
|
||||
|
||||
## Binary Ninja Installation
|
||||
|
||||
Lighthouse can be installed through the plugin manager on newer versions of Binary Ninja (>2.4.2918). The plugin will have to be installed manually on older versions.
|
||||
|
||||
### Auto Install
|
||||
Lighthouse can be installed through the plugin manager on Binary Ninja, supporting v3.5 and newer.
|
||||
|
||||
1. Open Binary Ninja's plugin manager by navigating the following submenus:
|
||||
- `Edit` -> `Preferences` -> `Manage Plugins`
|
||||
2. Search for Lighthouse in the plugin manager, and click the `Enable` button in the bottom right.
|
||||
3. Restart your disassembler.
|
||||
|
||||
### Manual Install
|
||||
|
||||
1. Open Binary Ninja's plugin folder by navigating the following submenus:
|
||||
- `Tools` -> `Open Plugins Folder...`
|
||||
2. Copy the contents of this repository's `/plugins/` folder to the listed directory.
|
||||
3. Restart your disassembler.
|
||||
|
||||
# Usage
|
||||
|
||||
Once properly installed, there will be a few new menu entries available in the disassembler. These are the entry points for a user to load coverage data and start using Lighthouse.
|
||||
@@ -203,6 +194,7 @@ Lighthouse will remember your theme preference for future loads and uses.
|
||||
|
||||
Time and motivation permitting, future work may include:
|
||||
|
||||
* Nag Vector35 to fix HLIL highlighting ([bug](https://github.com/Vector35/binaryninja-api/issues/2584)) in Binary Ninja
|
||||
* ~~Asynchronous composition, painting, metadata collection~~
|
||||
* ~~Multifile/coverage support~~
|
||||
* Profiling based heatmaps/painting
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"description": "A Coverage Explorer for Reverse Engineers",
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"text": "Copyright (c) 2021> Markus Gaasedelen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
|
||||
"text": "Copyright (c) 2024> Markus Gaasedelen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
|
||||
},
|
||||
"longdescription": "",
|
||||
"minimumbinaryninjaversion": 2918,
|
||||
"minimumbinaryninjaversion": 4526,
|
||||
"name": "Lighthouse",
|
||||
"platforms": [
|
||||
"Darwin",
|
||||
@@ -20,5 +20,5 @@
|
||||
"type": [
|
||||
"helper"
|
||||
],
|
||||
"version": "0.9.2"
|
||||
}
|
||||
"version": "0.9.4"
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ maps.map(function (e) {
|
||||
var filtered_maps = new ModuleMap(function (m) {
|
||||
if (whitelist.indexOf('all') >= 0) { return true; }
|
||||
|
||||
return whitelist.indexOf(m.name) >= 0;
|
||||
return whitelist.some(item => m.name.toLowerCase().includes(item.toLowerCase()));
|
||||
});
|
||||
|
||||
// This function takes a list of GumCompileEvents and converts it into a DRcov
|
||||
|
||||
+13
-5
@@ -1,6 +1,6 @@
|
||||
# CodeCoverage Pintool
|
||||
|
||||
The `CodeCoverage` pintool runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework and collects code coverage data in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). The log produced by this pintool emulates that of [drcov](http://dynamorio.org/docs/page_drcov.html) as shipped with [DynamoRIO](http://www.dynamorio.org).
|
||||
The `CodeCoverage` pintool runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework and collects code coverage data in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). The log produced by this pintool emulates that of [drcov](http://dynamorio.org/docs/page_drcov.html) as shipped with [DynamoRIO](http://www.dynamorio.org).
|
||||
|
||||
This pintool is labeled only as a prototype.
|
||||
|
||||
@@ -12,7 +12,7 @@ Follow the build instructions below for your respective platform.
|
||||
|
||||
## Building for MacOS or Linux
|
||||
|
||||
On MacOS or Liunux, one can compile the pintool using the following commands.
|
||||
On MacOS or Linux, one can compile the pintool using the following commands.
|
||||
|
||||
```
|
||||
# Location of this repo / pintool source
|
||||
@@ -39,7 +39,11 @@ Launch a command prompt and build the pintool with the following commands.
|
||||
### 32bit Pintool
|
||||
|
||||
```
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86
|
||||
REM If you are on VS 2022 or so you can run this line:
|
||||
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x86
|
||||
|
||||
REM VS 2015 or so you can run this line instead:
|
||||
REM "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86
|
||||
|
||||
REM Location of this repo / pintool source
|
||||
cd C:\Users\user\lighthouse\coverage\pin
|
||||
@@ -53,7 +57,11 @@ build-x86.bat
|
||||
### 64bit Pintool
|
||||
|
||||
```
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64
|
||||
REM If you are on VS 2022 or so you can run this line:
|
||||
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x86_amd64
|
||||
|
||||
REM VS 2015 or so you can run this line instead:
|
||||
REM "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64
|
||||
|
||||
REM Location of this repo / pintool source
|
||||
cd C:\Users\user\lighthouse\coverage\pin
|
||||
@@ -64,7 +72,7 @@ set PATH=%PATH%;%PIN_ROOT%
|
||||
build-x64.bat
|
||||
```
|
||||
|
||||
The resulting binaries will be labaled based on their architecture (eg, 64 is the 64bit pintool).
|
||||
The resulting binaries will be labeled based on their architecture (eg, 64 is the 64bit pintool).
|
||||
|
||||
* CodeCoverage.dll
|
||||
* CodeCoverage64.dll
|
||||
|
||||
@@ -27,7 +27,7 @@ link ^
|
||||
/LIBPATH:%PIN_ROOT%\intel64\lib ^
|
||||
/LIBPATH:"%PIN_ROOT%\intel64\lib-ext" ^
|
||||
/LIBPATH:"%PIN_ROOT%\extras\xed-intel64\lib" ^
|
||||
/LIBPATH:%PIN_ROOT%\intel64\runtime\pincrt pin.lib xed.lib pinvm.lib pincrt.lib ntdll-64.lib kernel32.lib crtbeginS.obj ^
|
||||
/LIBPATH:%PIN_ROOT%\intel64\runtime\pincrt pin.lib xed.lib pinipc.lib pincrt.lib kernel32.lib crtbeginS.obj ^
|
||||
/NODEFAULTLIB ^
|
||||
/MANIFEST:NO ^
|
||||
/OPT:NOREF ^
|
||||
|
||||
@@ -28,7 +28,7 @@ link ^
|
||||
/LIBPATH:%PIN_ROOT%\ia32\lib ^
|
||||
/LIBPATH:"%PIN_ROOT%\ia32\lib-ext" ^
|
||||
/LIBPATH:"%PIN_ROOT%\extras\xed-ia32\lib" ^
|
||||
/LIBPATH:%PIN_ROOT%\ia32\runtime\pincrt pin.lib xed.lib pinvm.lib pincrt.lib ntdll-32.lib kernel32.lib crtbeginS.obj ^
|
||||
/LIBPATH:%PIN_ROOT%\ia32\runtime\pincrt pin.lib xed.lib pinipc.lib pincrt.lib kernel32.lib crtbeginS.obj ^
|
||||
/NODEFAULTLIB ^
|
||||
/MANIFEST:NO ^
|
||||
/OPT:NOREF ^
|
||||
|
||||
@@ -1045,7 +1045,7 @@ class ComposingLine(QtWidgets.QPlainTextEdit):
|
||||
# set the height of the textbox based on some arbitrary math :D
|
||||
LINE_PADDING = self.document().documentMargin()*2
|
||||
line_height = self._font_metrics.height() + LINE_PADDING + 2
|
||||
self.setFixedHeight(line_height)
|
||||
self.setFixedHeight(int(line_height))
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# QPlainTextEdit Overloads
|
||||
|
||||
@@ -2,12 +2,11 @@ import os
|
||||
import time
|
||||
import logging
|
||||
import weakref
|
||||
import datetime
|
||||
import itertools
|
||||
import collections
|
||||
|
||||
from lighthouse.util import *
|
||||
from lighthouse.util.qt import compute_color_on_gradiant
|
||||
from lighthouse.util.qt import compute_color_on_gradient
|
||||
from lighthouse.metadata import DatabaseMetadata
|
||||
|
||||
logger = logging.getLogger("Lighthouse.Coverage")
|
||||
@@ -97,7 +96,7 @@ class DatabaseCoverage(object):
|
||||
# the addresses executed in the coverage log
|
||||
#
|
||||
|
||||
self._hitmap = build_hitmap(data)
|
||||
self._hitmap = collections.Counter(data)
|
||||
self._imagebase = BADADDR
|
||||
|
||||
#
|
||||
@@ -140,9 +139,7 @@ class DatabaseCoverage(object):
|
||||
# initially, all loaded coverage data is marked as unmapped
|
||||
#
|
||||
|
||||
self._unmapped_data = set(self._hitmap.keys())
|
||||
self._unmapped_data.add(BADADDR)
|
||||
self._misaligned_data = set()
|
||||
self.unmapped_addresses = set(self._hitmap.keys())
|
||||
|
||||
#
|
||||
# at runtime, the map_coverage() member function of this class is
|
||||
@@ -166,9 +163,14 @@ class DatabaseCoverage(object):
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instruction_percent = 0.0
|
||||
|
||||
# blocks that have not been fully executed (eg, crash / exception)
|
||||
self.partial_nodes = set()
|
||||
self.partial_instructions = set()
|
||||
|
||||
# addresses that have been executed, but are not in a defined node
|
||||
self.orphan_addresses = set()
|
||||
|
||||
#
|
||||
# we instantiate a single weakref of ourself (the DatbaseCoverage
|
||||
# object) such that we can distribute it to the children we create
|
||||
@@ -191,7 +193,7 @@ class DatabaseCoverage(object):
|
||||
@property
|
||||
def coverage(self):
|
||||
"""
|
||||
Return the instruction-level coverage bitmap/mask.
|
||||
Return the coverage (address) bitmap/mask.
|
||||
"""
|
||||
return viewkeys(self._hitmap)
|
||||
|
||||
@@ -266,6 +268,7 @@ class DatabaseCoverage(object):
|
||||
|
||||
if self._imagebase == BADADDR:
|
||||
self._imagebase = self._metadata.imagebase
|
||||
self._normalize_coverage()
|
||||
|
||||
#
|
||||
# if the imagebase for this coverage exists, then it is susceptible to
|
||||
@@ -301,9 +304,6 @@ class DatabaseCoverage(object):
|
||||
# update the coverage hash incase the hitmap changed
|
||||
self._update_coverage_hash()
|
||||
|
||||
# dump the unmappable coverage data
|
||||
#self.dump_unmapped()
|
||||
|
||||
def refresh_theme(self):
|
||||
"""
|
||||
Refresh UI facing elements to reflect the current theme.
|
||||
@@ -311,7 +311,7 @@ class DatabaseCoverage(object):
|
||||
Does not require @disassembler.execute_ui decorator as no Qt is touched.
|
||||
"""
|
||||
for function in self.functions.values():
|
||||
function.coverage_color = compute_color_on_gradiant(
|
||||
function.coverage_color = compute_color_on_gradient(
|
||||
function.instruction_percent,
|
||||
self.palette.table_coverage_bad,
|
||||
self.palette.table_coverage_good
|
||||
@@ -390,7 +390,7 @@ class DatabaseCoverage(object):
|
||||
self._update_coverage_hash()
|
||||
|
||||
# mark these touched addresses as dirty
|
||||
self._unmapped_data |= viewkeys(data)
|
||||
self.unmapped_addresses |= viewkeys(data)
|
||||
|
||||
def add_addresses(self, addresses, update=True):
|
||||
"""
|
||||
@@ -409,7 +409,7 @@ class DatabaseCoverage(object):
|
||||
self._update_coverage_hash()
|
||||
|
||||
# mark these touched addresses as dirty
|
||||
self._unmapped_data |= set(addresses)
|
||||
self.unmapped_addresses |= set(addresses)
|
||||
|
||||
def subtract_data(self, data):
|
||||
"""
|
||||
@@ -468,6 +468,60 @@ class DatabaseCoverage(object):
|
||||
# Coverage Mapping
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def _normalize_coverage(self):
|
||||
"""
|
||||
Normalize basic block coverage into instruction coverage.
|
||||
|
||||
TODO: It would be interesting if we could do away with this entirely,
|
||||
working off the original instruction/bb coverage data (hitmap) instead.
|
||||
"""
|
||||
coverage_addresses = viewkeys(self._hitmap)
|
||||
if not coverage_addresses:
|
||||
return
|
||||
|
||||
# bucketize the exploded coverage addresses
|
||||
instructions = coverage_addresses & self._metadata.instructions
|
||||
basic_blocks = instructions & viewkeys(self._metadata.nodes)
|
||||
|
||||
#
|
||||
# here we attempt to compute the ratio between basic block addresses,
|
||||
# and instruction addresses in the incoming coverage data.
|
||||
#
|
||||
# this will help us determine if the existing instruction data is
|
||||
# sufficient, or whether we need to explode/flatten the basic block
|
||||
# addresses into their respective child instructions
|
||||
#
|
||||
|
||||
block_ratio = len(basic_blocks) / float(len(instructions))
|
||||
block_trace_confidence = 0.80
|
||||
logger.debug("Block confidence %f" % block_ratio)
|
||||
|
||||
#
|
||||
# a low basic block to instruction ratio implies the data is probably
|
||||
# from an instruction trace, or a drcov trace that was exploded from
|
||||
# (bb_address, size) into its respective addresses
|
||||
#
|
||||
|
||||
if block_ratio < block_trace_confidence:
|
||||
return
|
||||
|
||||
#
|
||||
# take each basic block address, and explode it into a list of all the
|
||||
# instruction addresses contained within the basic block as determined
|
||||
# by the database metadata cache
|
||||
#
|
||||
# it is *possible* that this may introduce 'inaccurate' paint should
|
||||
# the user provide a basic block trace that crashes mid-block. but
|
||||
# that is not something we can account for in a block trace...
|
||||
#
|
||||
|
||||
for bb_address in basic_blocks:
|
||||
bb_hits = self._hitmap[bb_address]
|
||||
for inst_address in self._metadata.nodes[bb_address].instructions:
|
||||
self._hitmap[inst_address] = bb_hits
|
||||
|
||||
logger.debug("Converted basic block trace to instruction trace...")
|
||||
|
||||
def _map_coverage(self):
|
||||
"""
|
||||
Map loaded coverage data to the underlying database metadata.
|
||||
@@ -480,10 +534,11 @@ class DatabaseCoverage(object):
|
||||
"""
|
||||
Map loaded coverage data to database defined nodes (basic blocks).
|
||||
"""
|
||||
db_metadata = self._metadata
|
||||
dirty_nodes = {}
|
||||
|
||||
# the coverage data we will attempt to process in this function
|
||||
coverage_addresses = collections.deque(sorted(self._unmapped_data))
|
||||
coverage_addresses = sorted(self.unmapped_addresses)
|
||||
|
||||
#
|
||||
# the loop below is the core of our coverage mapping process.
|
||||
@@ -501,23 +556,27 @@ class DatabaseCoverage(object):
|
||||
# speed. please be careful if you wish to modify it...
|
||||
#
|
||||
|
||||
while coverage_addresses:
|
||||
i, num_addresses = 0, len(coverage_addresses)
|
||||
|
||||
while i < num_addresses:
|
||||
|
||||
# get the next coverage address to map
|
||||
address = coverage_addresses.popleft()
|
||||
address = coverage_addresses[i]
|
||||
|
||||
# get the node (basic block) metadata that this address falls in
|
||||
node_metadata = self._metadata.get_node(address)
|
||||
node_metadata = db_metadata.get_node(address)
|
||||
|
||||
#
|
||||
# should we fail to locate node metadata for the coverage address
|
||||
# that we are trying to map, then the address must not fall inside
|
||||
# of a defined function.
|
||||
#
|
||||
# in this case, the coverage address will remain unmapped...
|
||||
# of a defined function
|
||||
#
|
||||
|
||||
if not node_metadata:
|
||||
self.orphan_addresses.add(address)
|
||||
if address in db_metadata.instructions:
|
||||
self.unmapped_addresses.discard(address)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
#
|
||||
@@ -540,6 +599,10 @@ class DatabaseCoverage(object):
|
||||
node_coverage = NodeCoverage(node_metadata.address, self._weak_self)
|
||||
self.nodes[node_metadata.address] = node_coverage
|
||||
|
||||
# alias for speed, prior to looping
|
||||
node_start = node_metadata.address
|
||||
node_end = node_start + node_metadata.size
|
||||
|
||||
#
|
||||
# the loop below is as an inlined fast-path that assumes the next
|
||||
# several coverage addresses will likely belong to the same node
|
||||
@@ -552,29 +615,32 @@ class DatabaseCoverage(object):
|
||||
while 1:
|
||||
|
||||
#
|
||||
# map the hitmap data for the current address (an instruction)
|
||||
# to this NodeCoverage and mark the instruction as mapped by
|
||||
# discarding its address from the unmapped data list
|
||||
# map the hitmap data for the current address if it falls on
|
||||
# an actual instruction start within the node
|
||||
#
|
||||
# if the address falls within an instruction, it will just be
|
||||
# 'ignored', remaining in the 'unmapped' / invisible data
|
||||
#
|
||||
|
||||
node_coverage.executed_instructions[address] = self._hitmap[address]
|
||||
self._unmapped_data.discard(address)
|
||||
if address in node_metadata.instructions:
|
||||
node_coverage.executed_instructions[address] = self._hitmap[address]
|
||||
self.unmapped_addresses.discard(address)
|
||||
|
||||
# get the next address to attempt mapping on
|
||||
try:
|
||||
address = coverage_addresses.popleft()
|
||||
i += 1
|
||||
address = coverage_addresses[i]
|
||||
|
||||
# an IndexError implies there is nothing left to map...
|
||||
except IndexError:
|
||||
break;
|
||||
break
|
||||
|
||||
#
|
||||
# if the next address is not in this node, it's time break out
|
||||
# of this loop and send it through the full node lookup path
|
||||
#
|
||||
|
||||
if not (address in node_metadata.instructions):
|
||||
coverage_addresses.appendleft(address)
|
||||
if not (node_start <= address < node_end):
|
||||
break
|
||||
|
||||
# the node was updated, so save its coverage as dirty
|
||||
@@ -642,27 +708,10 @@ class DatabaseCoverage(object):
|
||||
self.functions = {}
|
||||
self.partial_nodes = set()
|
||||
self.partial_instructions = set()
|
||||
self._misaligned_data = set()
|
||||
self.orphan_addresses = set()
|
||||
|
||||
# dump the source coverage data back into an 'unmapped' state
|
||||
self._unmapped_data = set(self._hitmap.keys())
|
||||
self._unmapped_data.add(BADADDR)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Debug
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def dump_unmapped(self):
|
||||
"""
|
||||
Dump the unmapped coverage data.
|
||||
"""
|
||||
lmsg("Unmapped coverage data for %s:" % self.name)
|
||||
if len(self._unmapped_data) == 1: # 1 is going to be BADADDR
|
||||
lmsg(" * (there is no unmapped data!)")
|
||||
return
|
||||
|
||||
for address in self._unmapped_data:
|
||||
lmsg(" * 0x%X" % address)
|
||||
self.unmapped_addresses = set(self._hitmap.keys())
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Function Coverage
|
||||
@@ -749,7 +798,7 @@ class FunctionCoverage(object):
|
||||
self.executions = float(node_sum) / function_metadata.node_count
|
||||
|
||||
# bake colors
|
||||
self.coverage_color = compute_color_on_gradiant(
|
||||
self.coverage_color = compute_color_on_gradient(
|
||||
self.instruction_percent,
|
||||
self.database.palette.table_coverage_bad,
|
||||
self.database.palette.table_coverage_good
|
||||
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
import string
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import collections
|
||||
|
||||
from lighthouse.util.misc import *
|
||||
@@ -338,7 +337,7 @@ class CoverageDirector(object):
|
||||
"""
|
||||
Subscribe a callback for director refresh events.
|
||||
"""
|
||||
register_callback(self._refreshed_callbacks, callback)
|
||||
return register_callback(self._refreshed_callbacks, callback)
|
||||
|
||||
def _notify_refreshed(self):
|
||||
"""
|
||||
@@ -346,6 +345,13 @@ class CoverageDirector(object):
|
||||
"""
|
||||
notify_callback(self._refreshed_callbacks)
|
||||
|
||||
def unregister_refreshed(self, callback_ref):
|
||||
"""
|
||||
Unsubscribe a callback reference from director refresh events.
|
||||
"""
|
||||
|
||||
unregister_callback(self._refreshed_callbacks, callback_ref)
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Batch Loading
|
||||
#----------------------------------------------------------------------
|
||||
@@ -417,9 +423,8 @@ class CoverageDirector(object):
|
||||
if not aggregate_addresses:
|
||||
return (None, errors)
|
||||
|
||||
# optimize the aggregated data (once) and save it to the director
|
||||
coverage_data = self._optimize_coverage_data(aggregate_addresses)
|
||||
coverage = self.create_coverage(batch_name, coverage_data)
|
||||
# save the batched coverage data to the director
|
||||
coverage = self.create_coverage(batch_name, aggregate_addresses)
|
||||
|
||||
# evaluate coverage
|
||||
if not coverage.nodes:
|
||||
@@ -472,7 +477,6 @@ class CoverageDirector(object):
|
||||
try:
|
||||
coverage_file = self.reader.open(filepath)
|
||||
coverage_addresses = self._extract_coverage_data(coverage_file)
|
||||
coverage_data = self._optimize_coverage_data(coverage_addresses)
|
||||
|
||||
# save and suppress warnings generated from loading coverage files
|
||||
except CoverageParsingError as e:
|
||||
@@ -484,18 +488,18 @@ class CoverageDirector(object):
|
||||
errors[CoverageMissingError].append(CoverageMissingError(filepath))
|
||||
continue
|
||||
|
||||
# save the attribution data for this coverage data
|
||||
for address in coverage_data:
|
||||
if address in self.metadata.nodes:
|
||||
self.owners[address].add(filepath)
|
||||
|
||||
#
|
||||
# request a name for the new coverage mapping that the director will
|
||||
# generate from the loaded coverage data
|
||||
#
|
||||
|
||||
coverage_name = self._suggest_coverage_name(filepath)
|
||||
coverage = self.create_coverage(coverage_name, coverage_data, filepath)
|
||||
coverage = self.create_coverage(coverage_name, coverage_addresses, filepath)
|
||||
|
||||
# save the attribution data for this coverage data
|
||||
for address in coverage.data:
|
||||
if address in self.metadata.nodes: # TODO/UNMAPPED: support right click unmapped addrs
|
||||
self.owners[address].add(filepath)
|
||||
|
||||
# evaluate coverage
|
||||
if not coverage.nodes:
|
||||
@@ -621,81 +625,6 @@ class CoverageDirector(object):
|
||||
# well, this one is probably the fault of the CoverageFile author...
|
||||
raise NotImplementedError("Incomplete CoverageFile implementation")
|
||||
|
||||
def _optimize_coverage_data(self, coverage_addresses):
|
||||
"""
|
||||
Optimize exploded coverage data to the current metadata cache.
|
||||
"""
|
||||
logger.debug("Optimizing coverage data...")
|
||||
addresses = set(coverage_addresses)
|
||||
|
||||
# bucketize the exploded coverage addresses
|
||||
instructions = addresses & set(self.metadata.instructions)
|
||||
basic_blocks = instructions & viewkeys(self.metadata.nodes)
|
||||
|
||||
if not instructions:
|
||||
logger.debug("No mappable instruction addresses in coverage data")
|
||||
return []
|
||||
|
||||
"""
|
||||
#
|
||||
# TODO/LOADING: display undefined/misaligned data somehow?
|
||||
#
|
||||
|
||||
unknown = addresses - instructions
|
||||
|
||||
# bucketize the uncategorized exploded addresses
|
||||
undefined, misaligned = [], []
|
||||
for address in unknown:
|
||||
|
||||
# size == -1 (undefined inst)
|
||||
if self.metadata.get_instruction_size(address):
|
||||
undefined.append(address)
|
||||
|
||||
# size == 0 (misaligned inst)
|
||||
else:
|
||||
misaligned.append(address)
|
||||
"""
|
||||
|
||||
#
|
||||
# here we attempt to compute the ratio between basic block addresses,
|
||||
# and instruction addresses in the incoming coverage data.
|
||||
#
|
||||
# this will help us determine if the existing instruction data is
|
||||
# sufficient, or whether we need to explode/flatten the basic block
|
||||
# addresses into their respective child instructions
|
||||
#
|
||||
|
||||
block_ratio = len(basic_blocks) / float(len(instructions))
|
||||
block_trace_confidence = 0.80
|
||||
logger.debug("Block confidence %f" % block_ratio)
|
||||
|
||||
#
|
||||
# a low basic block to instruction ratio implies the data is probably
|
||||
# from an instruction trace, or a basic block trace has been flattened
|
||||
# exploded already (eg, a drcov log)
|
||||
#
|
||||
|
||||
if block_ratio < block_trace_confidence:
|
||||
logger.debug("Optimized as instruction trace...")
|
||||
return list(instructions)
|
||||
|
||||
#
|
||||
# take each basic block address, and explode it into a list of all the
|
||||
# instruction addresses contained within the basic block as determined
|
||||
# by the database metadata cache
|
||||
#
|
||||
# it is *possible* that this may introduce 'inaccurate' paint should
|
||||
# the user provide a basic block trace that crashes mid-block. but
|
||||
# that is not something we can account for in a block trace...
|
||||
#
|
||||
|
||||
block_instructions = set([])
|
||||
for address in basic_blocks:
|
||||
block_instructions |= set(self.metadata.nodes[address].instructions)
|
||||
|
||||
logger.debug("Optimized as basic block trace...")
|
||||
return list(block_instructions | instructions)
|
||||
|
||||
def _suggest_coverage_name(self, filepath):
|
||||
"""
|
||||
Return a suggested coverage name for the given filepath.
|
||||
@@ -831,7 +760,7 @@ class CoverageDirector(object):
|
||||
|
||||
def get_address_coverage(self, address):
|
||||
"""
|
||||
Return a list of coverage object containing the given address.
|
||||
Return a list of database coverage objects containing the given address.
|
||||
"""
|
||||
found = []
|
||||
|
||||
@@ -1052,12 +981,6 @@ class CoverageDirector(object):
|
||||
|
||||
return "%s - %s%% - %s" % (symbol, percent_str, coverage_name)
|
||||
|
||||
def dump_unmapped(self):
|
||||
"""
|
||||
Dump the unmapped coverage data for the active set.
|
||||
"""
|
||||
self.coverage.dump_unmapped()
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Aliases
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
@@ -56,7 +56,7 @@ class LighthouseBinja(LighthouseCore):
|
||||
# starts trying to use lighthouse for their session.
|
||||
#
|
||||
# so we initialize the lighthouse context (with start()) on the
|
||||
# second context request which will go throught the else block
|
||||
# second context request which will go through the else block
|
||||
# below... any subsequent call to start() is effectively a nop!
|
||||
#
|
||||
|
||||
@@ -96,16 +96,23 @@ class LighthouseBinja(LighthouseCore):
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# TODO / HACK / XXX / V35: Some of Binja's UI elements (such as the
|
||||
# TODO / HACK / XXX / V35 / 2021: Some of Binja's UI elements (such as the
|
||||
# terminal) do not get assigned a BV, even if there is only one open.
|
||||
#
|
||||
# this is problematic, because if the user 'clicks' onto the termial, and
|
||||
# this is problematic, because if the user 'clicks' onto the terminal, and
|
||||
# then tries to execute our UIActions (like 'Load Coverage File'), the
|
||||
# given 'context.binaryView' will be None
|
||||
#
|
||||
# in the meantime, we have to use this workaround that will try to grab
|
||||
# the 'current' bv from the dock. this is not ideal, but it will suffice.
|
||||
#
|
||||
# -----------------
|
||||
#
|
||||
# XXX: It's now 2024, Binja's UI / API stack has grown a lot. it's more
|
||||
# powerful and a bunch of the oddities / hacks lighthouse employed for
|
||||
# binja may no longer apply. this whole file should probably be revisited
|
||||
# and re-factored at some point point.. sorry if it's hard to follow
|
||||
#
|
||||
|
||||
def _interactive_load_file(self, context):
|
||||
dctx = disassembler.binja_get_bv_from_dock()
|
||||
@@ -121,8 +128,58 @@ class LighthouseBinja(LighthouseCore):
|
||||
return
|
||||
super(LighthouseBinja, self).interactive_load_batch(dctx)
|
||||
|
||||
def _open_coverage_xref(self, dctx, addr):
|
||||
super(LighthouseBinja, self).open_coverage_xref(addr, dctx)
|
||||
def _open_coverage_xref(self, context):
|
||||
super(LighthouseBinja, self).open_coverage_xref(context.address, context.binaryView)
|
||||
|
||||
def _interactive_coverage_xref(self, context):
|
||||
|
||||
if context is None:
|
||||
return
|
||||
|
||||
#
|
||||
# this is a special case where we check if the ctx exists rather than
|
||||
# blindly creating a new one. again, this is because binja may call
|
||||
# this function at random times to decide whether it should display the
|
||||
# XREF menu option.
|
||||
#
|
||||
# but asking whether or not the xref menu option should be shown is not
|
||||
# a good indication of 'is the user actually using lighthouse' so we
|
||||
# do not want this to be one that creates lighthouse contexts
|
||||
#
|
||||
|
||||
dctx = context.binaryView
|
||||
if not dctx:
|
||||
return
|
||||
|
||||
dctx_id = ctypes.addressof(dctx.handle.contents)
|
||||
lctx = self.lighthouse_contexts.get(dctx_id, None)
|
||||
if not lctx:
|
||||
return
|
||||
|
||||
#
|
||||
# is there even any coverage loaded into lighthouse? if not, the user
|
||||
# probably isn't even using it. so don't bother showing the xref action
|
||||
#
|
||||
|
||||
if not lctx.director.coverage_names:
|
||||
return
|
||||
|
||||
if context.view is None:
|
||||
return
|
||||
|
||||
view = context.view
|
||||
context_menu = view.contextMenu()
|
||||
|
||||
#
|
||||
# Create a new, temporary Coverage Xref action to inject into the
|
||||
# right click context menu that is being shown...
|
||||
#
|
||||
|
||||
action = "Coverage Xref"
|
||||
UIAction.registerAction(action)
|
||||
action_handler = view.actionHandler()
|
||||
action_handler.bindAction(action, UIAction(self._open_coverage_xref))
|
||||
context_menu.addAction(action, "Plugins")
|
||||
|
||||
def _is_xref_valid(self, dctx, addr):
|
||||
|
||||
@@ -152,6 +209,12 @@ class LighthouseBinja(LighthouseCore):
|
||||
return
|
||||
super(LighthouseBinja, self).open_coverage_overview(dctx)
|
||||
|
||||
def _stub(self, context):
|
||||
# XXX: This was added as a last minute bodge prior to releasing v0.9.3,
|
||||
# it fixes a crash-on-close that was manifesting on binja macOS, when
|
||||
# using a lambda instead of a concrete function/stub like this.
|
||||
return None
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Binja Actions
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -165,31 +228,28 @@ class LighthouseBinja(LighthouseCore):
|
||||
action = self.ACTION_LOAD_FILE
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_file))
|
||||
Menu.mainMenu("Tools").addAction(action, "Loading", 0)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 0)
|
||||
logger.info("Installed the 'Code coverage file' menu entry")
|
||||
|
||||
def _install_load_batch(self):
|
||||
action = self.ACTION_LOAD_BATCH
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_batch))
|
||||
Menu.mainMenu("Tools").addAction(action, "Loading", 1)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 1)
|
||||
logger.info("Installed the 'Code coverage batch' menu entry")
|
||||
|
||||
# TODO/V35: convert to a UI action once we can disable/disable them on the fly
|
||||
def _install_open_coverage_xref(self):
|
||||
PluginCommand.register_for_address(
|
||||
self.ACTION_COVERAGE_XREF,
|
||||
"Open the coverage xref window",
|
||||
self._open_coverage_xref,
|
||||
self._is_xref_valid
|
||||
)
|
||||
action = self.ACTION_COVERAGE_XREF
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._stub, self._interactive_coverage_xref))
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 2)
|
||||
|
||||
# NOTE/V35: Binja automatically creates View --> Show Coverage Overview
|
||||
def _install_open_coverage_overview(self):
|
||||
action = self.ACTION_COVERAGE_OVERVIEW
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._open_coverage_overview))
|
||||
Menu.mainMenu("Tools").addAction(action, "Windows", 0)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Windows", 0)
|
||||
logger.info("Installed the 'Open Coverage Overview' menu entry")
|
||||
|
||||
# NOTE/V35: Binja doesn't really 'unload' plugins, so whatever...
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import abc
|
||||
import logging
|
||||
|
||||
@@ -7,10 +6,10 @@ import lighthouse
|
||||
from lighthouse.util import lmsg
|
||||
from lighthouse.util.qt import *
|
||||
from lighthouse.util.update import check_for_update
|
||||
from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI
|
||||
from lighthouse.util.disassembler import disassembler
|
||||
|
||||
from lighthouse.ui import *
|
||||
from lighthouse.metadata import DatabaseMetadata, metadata_progress
|
||||
from lighthouse.metadata import metadata_progress
|
||||
from lighthouse.exceptions import *
|
||||
|
||||
logger = logging.getLogger("Lighthouse.Core")
|
||||
@@ -26,9 +25,9 @@ class LighthouseCore(object):
|
||||
# Plugin Metadata
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
PLUGIN_VERSION = "0.9.2"
|
||||
PLUGIN_VERSION = "0.9.4-DEV"
|
||||
AUTHORS = "Markus Gaasedelen"
|
||||
DATE = "2021"
|
||||
DATE = "2024"
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Initialization
|
||||
@@ -191,7 +190,8 @@ class LighthouseCore(object):
|
||||
"""
|
||||
for lctx in self.lighthouse_contexts.values():
|
||||
lctx.director.refresh_theme()
|
||||
lctx.coverage_overview.refresh_theme()
|
||||
if lctx.coverage_overview:
|
||||
lctx.coverage_overview.refresh_theme()
|
||||
lctx.painter.force_repaint()
|
||||
|
||||
def open_coverage_overview(self, dctx=None):
|
||||
|
||||
@@ -78,7 +78,7 @@ class DatabaseMetadata(object):
|
||||
# the cache of key database structures
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instructions = []
|
||||
self.instructions = set()
|
||||
|
||||
# internal members to help index & navigate the cached metadata
|
||||
self._name2func = {}
|
||||
@@ -152,14 +152,6 @@ class DatabaseMetadata(object):
|
||||
# Providers
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def get_instructions_slice(self, start_address, end_address):
|
||||
"""
|
||||
Get the instructions addresses that fall within a given range.
|
||||
"""
|
||||
index_start = bisect.bisect_left(self.instructions, start_address)
|
||||
index_end = bisect.bisect_left(self.instructions, end_address)
|
||||
return self.instructions[index_start:index_end]
|
||||
|
||||
def get_instruction_size(self, address):
|
||||
"""
|
||||
Get the size of an instruction at a given address.
|
||||
@@ -403,19 +395,6 @@ class DatabaseMetadata(object):
|
||||
if join:
|
||||
worker.join()
|
||||
|
||||
def _refresh_instructions(self):
|
||||
"""
|
||||
Refresh the list of database instructions (from function metadata).
|
||||
"""
|
||||
instructions = []
|
||||
for function_metadata in itervalues(self.functions):
|
||||
instructions.append(function_metadata.instructions)
|
||||
instructions = list(set(itertools.chain.from_iterable(instructions)))
|
||||
instructions.sort()
|
||||
|
||||
# commit the updated instruction list
|
||||
self.instructions = instructions
|
||||
|
||||
def _refresh_lookup(self):
|
||||
"""
|
||||
Refresh the internal fast lookup address lists.
|
||||
@@ -470,11 +449,11 @@ class DatabaseMetadata(object):
|
||||
|
||||
def _clear_cache(self):
|
||||
"""
|
||||
Cleare the metadata cache of all collected info.
|
||||
Clear the metadata cache of all collected info.
|
||||
"""
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instructions = []
|
||||
self.instructions = set()
|
||||
self._node2func = collections.defaultdict(list)
|
||||
self._refresh_lookup()
|
||||
self.cached = False
|
||||
@@ -520,9 +499,6 @@ class DatabaseMetadata(object):
|
||||
end = time.time()
|
||||
logger.debug("Metadata collection took %s seconds" % (end - start))
|
||||
|
||||
# regenerate the instruction list from collected metadata
|
||||
self._refresh_instructions()
|
||||
|
||||
# refresh the internal function/node fast lookup lists
|
||||
self._refresh_lookup()
|
||||
|
||||
@@ -574,6 +550,8 @@ class DatabaseMetadata(object):
|
||||
completed += CHUNK_SIZE if function_addresses else len(addresses_chunk)
|
||||
progress_callback(completed, total)
|
||||
|
||||
self._cache_instructions()
|
||||
|
||||
@not_mainthread
|
||||
def _async_collect_metadata(self, function_addresses, progress_callback):
|
||||
"""
|
||||
@@ -658,6 +636,59 @@ class DatabaseMetadata(object):
|
||||
self.nodes.update(function_metadata.nodes)
|
||||
self.functions[address] = function_metadata
|
||||
|
||||
def _cache_instructions(self):
|
||||
"""
|
||||
This will be replaced with a disassembler-specific function at runtime.
|
||||
|
||||
NOTE: Read the 'MONKEY PATCHING' section at the end of this file.
|
||||
"""
|
||||
raise RuntimeError("This function should have been monkey patched...")
|
||||
|
||||
def _binja_cache_instructions(self):
|
||||
"""
|
||||
Cache the list of instructions by doing a full scrape of the Binary Ninja database.
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
#
|
||||
# since 'code' does not exist outside of functions in binary ninja,
|
||||
# just scrape instructions from our existing cached nodes
|
||||
#
|
||||
|
||||
for function_metadata in itervalues(self.functions):
|
||||
instructions.append(function_metadata.instructions)
|
||||
|
||||
# commit the updated instruction list
|
||||
self.instructions = set(itertools.chain.from_iterable(instructions))
|
||||
|
||||
def _ida_cache_instructions(self):
|
||||
"""
|
||||
Cache the list of instructions by doing a full scrape of the IDA database.
|
||||
"""
|
||||
instructions = set()
|
||||
|
||||
# alias for speed
|
||||
ida_is_code = idaapi.is_code
|
||||
ida_get_flags = idaapi.get_flags
|
||||
ida_next_head = idaapi.next_head
|
||||
add_instruction = instructions.add
|
||||
|
||||
# scrape instruction addresses from the database
|
||||
for seg_address in idautils.Segments():
|
||||
seg = idaapi.getseg(seg_address)
|
||||
|
||||
current_address = seg_address
|
||||
end_address = seg.end_ea
|
||||
|
||||
# save the address of each defined instruction in the segment
|
||||
while current_address < end_address:
|
||||
if ida_is_code(ida_get_flags(current_address)):
|
||||
add_instruction(current_address)
|
||||
current_address = ida_next_head(current_address, end_address)
|
||||
|
||||
# commit the updated instruction set
|
||||
self.instructions = instructions
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Signal Handlers
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -905,7 +936,7 @@ class FunctionMetadata(object):
|
||||
|
||||
for i in range(0, count.value):
|
||||
if edges[i].target:
|
||||
function_metadata.edges[edge_src].append(node._create_instance(BNNewBasicBlockReference(edges[i].target), bv).start)
|
||||
function_metadata.edges[edge_src].append(node._create_instance(BNNewBasicBlockReference(edges[i].target)).start)
|
||||
core.BNFreeBasicBlockEdgeList(edges, count.value)
|
||||
|
||||
# NOTE/PERF ~28% of metadata collection time alone...
|
||||
@@ -1160,6 +1191,7 @@ def metadata_progress(completed, total):
|
||||
if disassembler.NAME == "IDA":
|
||||
import idaapi
|
||||
import idautils
|
||||
DatabaseMetadata._cache_instructions = DatabaseMetadata._ida_cache_instructions
|
||||
FunctionMetadata._refresh_nodes = FunctionMetadata._ida_refresh_nodes
|
||||
NodeMetadata._cache_node = NodeMetadata._ida_cache_node
|
||||
|
||||
@@ -1170,6 +1202,7 @@ elif disassembler.NAME == "BINJA":
|
||||
import ctypes
|
||||
import binaryninja
|
||||
from binaryninja import core
|
||||
DatabaseMetadata._cache_instructions = DatabaseMetadata._binja_cache_instructions
|
||||
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
|
||||
NodeMetadata._cache_node = NodeMetadata._binja_cache_node
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ class BinjaPainter(DatabasePainter):
|
||||
|
||||
def _clear_instructions(self, instructions):
|
||||
bv = disassembler[self.lctx].bv
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
for address in instructions:
|
||||
for func in bv.get_functions_containing(address):
|
||||
func.set_auto_instr_highlight(address, HighlightStandardColor.NoHighlightColor)
|
||||
@@ -45,6 +47,11 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_instructions -= set(instructions)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _partial_paint(self, bv, instructions, color):
|
||||
for address in instructions:
|
||||
for func in bv.get_functions_containing(address):
|
||||
@@ -57,6 +64,8 @@ class BinjaPainter(DatabasePainter):
|
||||
db_coverage = self.director.coverage
|
||||
db_metadata = self.director.metadata
|
||||
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
r, g, b, _ = self.palette.coverage_paint.getRgb()
|
||||
color = HighlightColor(red=r, green=g, blue=b)
|
||||
|
||||
@@ -83,10 +92,17 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_nodes |= (set(node_addresses) - partial_nodes)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _clear_nodes(self, node_addresses):
|
||||
bv = disassembler[self.lctx].bv
|
||||
db_metadata = self.director.metadata
|
||||
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
for node_address in node_addresses:
|
||||
node_metadata = db_metadata.nodes.get(node_address, None)
|
||||
|
||||
@@ -102,6 +118,11 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_nodes -= set(node_addresses)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _refresh_ui(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ class IDAPainter(DatabasePainter):
|
||||
|
||||
#
|
||||
# if we did not get *everything* that we needed, then it is
|
||||
# possible the database changesd, or the coverage set changed...
|
||||
# possible the database changed, or the coverage set changed...
|
||||
#
|
||||
# this is kind of what we get for not using locks :D but that's
|
||||
# okay, just stop painting here and let the painter sort it out
|
||||
|
||||
@@ -537,8 +537,6 @@ class DatabasePainter(object):
|
||||
a rebase occurs while the painter is running.
|
||||
"""
|
||||
db_metadata = self.director.metadata
|
||||
instructions = db_metadata.instructions
|
||||
nodes = viewvalues(db_metadata.nodes)
|
||||
|
||||
# a rebase has not occurred
|
||||
if not db_metadata.cached or (db_metadata.imagebase == self._imagebase):
|
||||
|
||||
@@ -100,7 +100,22 @@ class DrcovData(CoverageFile):
|
||||
mod_ids = [module.id for module in modules]
|
||||
|
||||
# loop through the coverage data and filter out data for the target ids
|
||||
coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
if self.version < 3:
|
||||
coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
|
||||
#
|
||||
# drcov version 3 does not include the 'preferred' / sub-module base
|
||||
# in the bb offset, so we must add that base offset before returning
|
||||
# the block offsets to correctly normalize things
|
||||
#
|
||||
# it's unclear if the preferred_base for given sub-module segments
|
||||
# will always be correct, so we opt to simply use the first segment
|
||||
# in a given module as the base to compute the known runtime offset
|
||||
#
|
||||
|
||||
else:
|
||||
mod_bases = dict([(module.id, module.start - modules[0].start) for module in modules])
|
||||
coverage_blocks = [(mod_bases[bb.mod_id] + bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
|
||||
# return the filtered coverage blocks
|
||||
return coverage_blocks
|
||||
@@ -137,7 +152,7 @@ class DrcovData(CoverageFile):
|
||||
flavor_line = f.readline().decode('utf-8').strip()
|
||||
self.flavor = flavor_line.split(":")[1]
|
||||
|
||||
assert self.version == 2, "Only drcov version 2 log files supported"
|
||||
assert self.version == 2 or self.version == 3, "Only drcov versions 2 and 3 log files supported"
|
||||
|
||||
def _parse_module_table(self, f):
|
||||
"""
|
||||
@@ -227,6 +242,12 @@ class DrcovData(CoverageFile):
|
||||
Mac/Linux:
|
||||
'Columns: id, containing_id, start, end, entry, offset, path'
|
||||
|
||||
DynamoRIO v10.0.19734, table version 5:
|
||||
Windows:
|
||||
'Columns: id, containing_id, start, end, entry, offset, preferred_base, checksum, timestamp, path'
|
||||
Mac/Linux:
|
||||
'Columns: id, containing_id, start, end, entry, offset, preferred_base, path'
|
||||
|
||||
"""
|
||||
|
||||
# NOTE/COMPAT: there is no 'Columns' line for the v1 table...
|
||||
@@ -450,19 +471,19 @@ class DrcovModule(object):
|
||||
"""
|
||||
Parse a module table v5 entry.
|
||||
"""
|
||||
self.id = int(data[0])
|
||||
self.containing_id = int(data[1])
|
||||
self.base = int(data[2], 16)
|
||||
self.end = int(data[3], 16)
|
||||
self.entry = int(data[4], 16)
|
||||
self.offset = int(data[5], 16)
|
||||
self.preferred_base= int(data[6], 16)
|
||||
self.id = int(data[0])
|
||||
self.containing_id = int(data[1])
|
||||
self.base = int(data[2], 16)
|
||||
self.end = int(data[3], 16)
|
||||
self.entry = int(data[4], 16)
|
||||
self.offset = int(data[5], 16)
|
||||
self.preferred_base = int(data[6], 16)
|
||||
if len(data) > 8: # Windows Only
|
||||
self.checksum = int(data[7], 16)
|
||||
self.timestamp = int(data[8], 16)
|
||||
self.path = str(data[-1])
|
||||
self.size = self.end-self.base
|
||||
self.filename = os.path.basename(self.path.replace('\\', os.sep))
|
||||
self.checksum = int(data[7], 16)
|
||||
self.timestamp = int(data[8], 16)
|
||||
self.path = str(data[-1])
|
||||
self.size = self.end-self.base
|
||||
self.filename = os.path.basename(self.path.replace('\\', os.sep))
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
@@ -118,7 +118,7 @@ class CoverageComboBox(QtWidgets.QComboBox):
|
||||
|
||||
self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||
self.setMaximumHeight(self._font_metrics.height()*1.75)
|
||||
self.setMaximumHeight(int(self._font_metrics.height()*1.75))
|
||||
|
||||
# draw the QComboBox with a 'Windows'-esque style
|
||||
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
|
||||
@@ -437,10 +437,7 @@ class CoverageComboBoxView(QtWidgets.QTableView):
|
||||
#
|
||||
|
||||
hh.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
vh.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
hh.setMinimumSectionSize(0)
|
||||
vh.setMinimumSectionSize(0)
|
||||
|
||||
# get the column width hint from the model for the 'X' delete column
|
||||
@@ -451,7 +448,9 @@ class CoverageComboBoxView(QtWidgets.QTableView):
|
||||
)
|
||||
|
||||
# set the 'X' delete icon column width to a fixed size based on the hint
|
||||
hh.setMinimumSectionSize(icon_column_width)
|
||||
hh.resizeSection(COLUMN_DELETE, icon_column_width)
|
||||
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
# install a delegate to do some custom painting against the combobox
|
||||
self.setItemDelegate(ComboBoxDelegate(self))
|
||||
@@ -533,7 +532,7 @@ class CoverageComboBoxModel(QtCore.QAbstractTableModel):
|
||||
delete_icon = QtGui.QPixmap(plugin_resource("icons/delete_coverage.png"))
|
||||
|
||||
# compute the appropriate size for the deletion icon
|
||||
icon_height = self._font_metrics.height()*0.75
|
||||
icon_height = int(self._font_metrics.height()*0.75)
|
||||
icon_width = icon_height
|
||||
|
||||
# scale the icon as appropriate (very likely scaling it down)
|
||||
|
||||
@@ -29,6 +29,7 @@ class CoverageOverview(object):
|
||||
|
||||
self.lctx.coverage_overview = self
|
||||
self.initialized = False
|
||||
self._refreshed_callback = None
|
||||
|
||||
# see the EventProxy class below for more details
|
||||
self._events = EventProxy(self)
|
||||
@@ -42,7 +43,7 @@ class CoverageOverview(object):
|
||||
self.refresh()
|
||||
|
||||
# register for cues from the director
|
||||
self.director.refreshed(self.refresh)
|
||||
self._refreshed_callback = self.director.refreshed(self.refresh)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Pseudo Widget Functions
|
||||
@@ -64,11 +65,27 @@ class CoverageOverview(object):
|
||||
"""
|
||||
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
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -194,7 +211,7 @@ class CoverageOverview(object):
|
||||
|
||||
# layout the major elements of our widget
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setSpacing(get_dpi_scale()*5.0)
|
||||
layout.setSpacing(int(get_dpi_scale()*5))
|
||||
layout.addWidget(self._table_view)
|
||||
layout.addWidget(self._toolbar)
|
||||
|
||||
@@ -214,8 +231,8 @@ class CoverageOverview(object):
|
||||
-1*self._settings_menu.sizeHint().height()
|
||||
)
|
||||
center = QtCore.QPoint(
|
||||
self._settings_button.sizeHint().width()/2,
|
||||
self._settings_button.sizeHint().height()/2
|
||||
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)
|
||||
@@ -229,6 +246,9 @@ class CoverageOverview(object):
|
||||
"""
|
||||
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()
|
||||
@@ -238,6 +258,9 @@ class CoverageOverview(object):
|
||||
"""
|
||||
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()
|
||||
|
||||
@@ -68,10 +68,6 @@ class TableSettingsMenu(QtWidgets.QMenu):
|
||||
self._action_refresh_metadata.setToolTip("Refresh the database metadata and coverage mapping")
|
||||
self.addAction(self._action_refresh_metadata)
|
||||
|
||||
self._action_dump_unmapped = QtWidgets.QAction("Dump unmapped coverage", None)
|
||||
self._action_dump_unmapped.setToolTip("Print all coverage data not mapped to a function")
|
||||
self.addAction(self._action_dump_unmapped)
|
||||
|
||||
self._action_export_html = QtWidgets.QAction("Generate HTML report", None)
|
||||
self._action_export_html.setToolTip("Export the coverage table to HTML")
|
||||
self.addAction(self._action_export_html)
|
||||
@@ -91,7 +87,6 @@ class TableSettingsMenu(QtWidgets.QMenu):
|
||||
self._action_disable_paint.triggered[bool].connect(lambda x: lctx.painter.set_enabled(not x))
|
||||
self._action_force_clear.triggered.connect(lctx.painter.force_clear)
|
||||
self._action_export_html.triggered.connect(controller.export_to_html)
|
||||
self._action_dump_unmapped.triggered.connect(lctx.director.dump_unmapped)
|
||||
lctx.painter.status_changed(self._ui_painter_changed_status)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
@@ -145,7 +145,7 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
entry_rect = entry_fm.boundingRect(entry_text)
|
||||
|
||||
# select the larger of the two potential column widths
|
||||
column_width = max(title_rect.width(), entry_rect.width()*1.2)
|
||||
column_width = int(max(title_rect.width(), entry_rect.width()*1.2))
|
||||
|
||||
# save the final column width
|
||||
self.setColumnWidth(i, column_width)
|
||||
@@ -191,13 +191,17 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
# 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(entry_fm.height()+tweak)
|
||||
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)
|
||||
@@ -307,8 +311,8 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
"""
|
||||
|
||||
# get the list rows currently selected in the coverage table
|
||||
selected_rows = self.selectionModel().selectedRows()
|
||||
if len(selected_rows) == 0:
|
||||
selected_row_indexes = self.selectionModel().selectedRows()
|
||||
if len(selected_row_indexes) == 0:
|
||||
return None
|
||||
|
||||
# the context menu we will dynamically populate
|
||||
@@ -320,13 +324,25 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
# copy function name, address, or renaming the function.
|
||||
#
|
||||
|
||||
if len(selected_rows) == 1:
|
||||
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 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
|
||||
@@ -385,6 +401,14 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
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)
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -438,6 +462,9 @@ class CoverageTableController(object):
|
||||
|
||||
# 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
|
||||
@@ -536,6 +563,38 @@ class CoverageTableController(object):
|
||||
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
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -547,6 +606,8 @@ class CoverageTableController(object):
|
||||
|
||||
# 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
|
||||
@@ -608,9 +669,13 @@ class CoverageTableController(object):
|
||||
{
|
||||
"filter": "HTML Files (*.html)",
|
||||
"caption": "Save HTML Report",
|
||||
"directory": suggested_filepath
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -636,7 +701,8 @@ class CoverageTableController(object):
|
||||
function_addresses = []
|
||||
for row_number in rows:
|
||||
address = self._model.row2func[row_number]
|
||||
function_addresses.append(address)
|
||||
if address != BADADDR:
|
||||
function_addresses.append(address)
|
||||
return function_addresses
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
@@ -835,6 +901,87 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# 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.
|
||||
@@ -843,80 +990,31 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# a request has been made for what text to show in a table cell
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
|
||||
# alias the requested column number once, for readability & perf
|
||||
column = index.column()
|
||||
function_address = self.row2func[index.row()]
|
||||
|
||||
# lookup the function info for this row
|
||||
try:
|
||||
function_address = self.row2func[index.row()]
|
||||
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
|
||||
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
|
||||
@@ -953,6 +1051,13 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
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
|
||||
@@ -963,7 +1068,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
sorted_functions = sorted(
|
||||
itervalues(self._visible_metadata),
|
||||
key=attrgetter(sort_field),
|
||||
reverse=sort_order
|
||||
reverse=direction
|
||||
)
|
||||
|
||||
# sort the table entries by a function coverage attribute
|
||||
@@ -971,7 +1076,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
sorted_functions = sorted(
|
||||
itervalues(self._visible_coverage),
|
||||
key=attrgetter(sort_field),
|
||||
reverse=sort_order
|
||||
reverse=direction
|
||||
)
|
||||
|
||||
#
|
||||
@@ -989,7 +1094,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# items (0%) should be appended to the *end*
|
||||
#
|
||||
|
||||
if sort_order:
|
||||
if direction:
|
||||
sorted_functions += self._no_coverage
|
||||
|
||||
#
|
||||
@@ -1005,6 +1110,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -1316,6 +1422,9 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
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)}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
name_entry.setToolTip(coverage.filepath)
|
||||
self._table.setItem(i, 2, name_entry)
|
||||
date_entry = QtWidgets.QTableWidgetItem()
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(coverage.timestamp*1000))
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(int(coverage.timestamp*1000)))
|
||||
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(date_entry))
|
||||
|
||||
# filepaths
|
||||
@@ -135,7 +135,7 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
name_entry.setToolTip(filepath)
|
||||
self._table.setItem(i, 2, name_entry)
|
||||
date_entry = QtWidgets.QTableWidgetItem()
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(timestamp*1000))
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(int(timestamp*1000)))
|
||||
self._table.setItem(i, 3, date_entry)
|
||||
|
||||
self._table.resizeColumnsToContents()
|
||||
@@ -153,8 +153,8 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
layout.addWidget(self._table)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = get_dpi_scale() * 250
|
||||
width = get_dpi_scale() * 600
|
||||
height = int(get_dpi_scale() * 250)
|
||||
width = int(get_dpi_scale() * 600)
|
||||
self.setMinimumHeight(height)
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
|
||||
@@ -148,8 +148,8 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
layout.addWidget(self._checkbox_ignore_missing)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = get_dpi_scale() * 250
|
||||
width = get_dpi_scale() * 400
|
||||
height = int(get_dpi_scale() * 250)
|
||||
width = int(get_dpi_scale() * 400)
|
||||
self.setMinimumHeight(height)
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
import struct
|
||||
import glob
|
||||
import shutil
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# NOTE: Py2/Py3 compat
|
||||
try:
|
||||
@@ -260,8 +259,13 @@ class LighthousePalette(object):
|
||||
user_theme_dir = self.get_user_theme_dir()
|
||||
makedirs(user_theme_dir)
|
||||
|
||||
# enumerate all in-box / default themes
|
||||
plugin_theme_dir = self.get_plugin_theme_dir()
|
||||
json_files = glob.glob(os.path.join(plugin_theme_dir, "*.json"))
|
||||
|
||||
# copy the default themes into the user directory if they don't exist
|
||||
for theme_name in self._default_themes.values():
|
||||
for default_theme_file in json_files:
|
||||
theme_name = os.path.basename(default_theme_file)
|
||||
|
||||
#
|
||||
# check if lighthouse has copied the default themes into the user
|
||||
@@ -274,8 +278,7 @@ class LighthousePalette(object):
|
||||
continue
|
||||
|
||||
# copy the in-box themes to the user theme directory
|
||||
plugin_theme_file = os.path.join(self.get_plugin_theme_dir(), theme_name)
|
||||
shutil.copy(plugin_theme_file, user_theme_file)
|
||||
shutil.copy(default_theme_file, user_theme_file)
|
||||
|
||||
#
|
||||
# if the user tries to switch themes, ensure the file dialog will start
|
||||
@@ -546,7 +549,11 @@ class LighthousePalette(object):
|
||||
# lmao, don't ask me why they forgot about this attribute from 5.0 - 5.6
|
||||
#
|
||||
|
||||
if disassembler.NAME == "BINJA":
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if disassembler.NAME == "BINJA" or (disassembler.NAME == "IDA"
|
||||
and disassembler._version_major == 9
|
||||
and disassembler._version_minor >= 2):
|
||||
test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
|
||||
else:
|
||||
test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "Long Night",
|
||||
"author": "https://github.com/ioncodes",
|
||||
|
||||
"colors":
|
||||
{
|
||||
"black": [33, 33, 33],
|
||||
"white": [241, 239, 236],
|
||||
|
||||
"darkGray": [20, 20, 20],
|
||||
"darkGray2": [30, 30, 30],
|
||||
"darkGray3": [54, 54, 54],
|
||||
|
||||
"gray": [100, 100, 100],
|
||||
"lightGray": [55, 55, 55],
|
||||
|
||||
"red": [188, 101, 141],
|
||||
"green": [64, 255, 64],
|
||||
"blue": [104, 134, 197],
|
||||
"lightBlue": [128, 200, 255],
|
||||
"darkBlue": [44, 44, 44],
|
||||
"purple": [121, 104, 197],
|
||||
|
||||
"focusRed": [255, 83, 112],
|
||||
"selection": [67, 67, 67]
|
||||
},
|
||||
|
||||
"fields":
|
||||
{
|
||||
"coverage_paint": ["darkBlue", "lightBlue"],
|
||||
|
||||
"table_text": "white",
|
||||
"table_grid": "black",
|
||||
"table_coverage_none": "black",
|
||||
"table_coverage_bad": "red",
|
||||
"table_coverage_good": "blue",
|
||||
"table_background": "black",
|
||||
"table_selection": "purple",
|
||||
|
||||
"html_summary_text": "white",
|
||||
"html_table_header": "white",
|
||||
"html_page_background": "black",
|
||||
|
||||
"shell_text": "white",
|
||||
"shell_text_valid": "lightBlue",
|
||||
"shell_text_invalid": "red",
|
||||
"shell_highlight_invalid": "red",
|
||||
|
||||
"shell_border": "lightGray",
|
||||
"shell_border_focus": "focusRed",
|
||||
"shell_background": "black",
|
||||
|
||||
"shell_hint_text": "white",
|
||||
"shell_hint_background": "black",
|
||||
|
||||
"logic_token": "red",
|
||||
"comma_token": "green",
|
||||
"paren_token": "green",
|
||||
"coverage_token": "lightBlue",
|
||||
|
||||
"combobox_text": "white",
|
||||
"combobox_selection_text": "white",
|
||||
"combobox_selection_background": "selection",
|
||||
|
||||
"combobox_border": "lightGray",
|
||||
"combobox_border_focus": "focusRed",
|
||||
"combobox_background": "black"
|
||||
}
|
||||
}
|
||||
@@ -289,14 +289,18 @@ class BinjaContextAPI(DisassemblerContextAPI):
|
||||
|
||||
return vi.navigateToFunction(func, address)
|
||||
|
||||
@BinjaCoreAPI.execute_write
|
||||
def set_function_name_at(self, function_address, new_name):
|
||||
func = self.bv.get_function_at(function_address)
|
||||
|
||||
if not func:
|
||||
return
|
||||
|
||||
if new_name == "":
|
||||
new_name = None
|
||||
|
||||
state = self.bv.begin_undo_actions()
|
||||
func.name = new_name
|
||||
self.bv.commit_undo_actions(state)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Hooks API
|
||||
@@ -322,9 +326,6 @@ class RenameHooks(binaryview.BinaryDataNotification):
|
||||
|
||||
def __init__(self, bv):
|
||||
self._bv = bv
|
||||
self.symbol_added = self.__symbol_handler
|
||||
self.symbol_updated = self.__symbol_handler
|
||||
self.symbol_removed = self.__symbol_handler
|
||||
|
||||
def hook(self):
|
||||
self._bv.register_notification(self)
|
||||
@@ -332,11 +333,25 @@ class RenameHooks(binaryview.BinaryDataNotification):
|
||||
def unhook(self):
|
||||
self._bv.unregister_notification(self)
|
||||
|
||||
def __symbol_handler(self, view, symbol):
|
||||
def symbol_added(self, *args):
|
||||
self.__symbol_handler(*args)
|
||||
|
||||
def symbol_updated(self, *args):
|
||||
self.__symbol_handler(*args)
|
||||
|
||||
def symbol_removed(self, *args):
|
||||
self.__symbol_handler(*args, True)
|
||||
|
||||
def __symbol_handler(self, view, symbol, removed=False):
|
||||
|
||||
func = self._bv.get_function_at(symbol.address)
|
||||
if not func.start == symbol.address:
|
||||
if not func or not func.start == symbol.address:
|
||||
return
|
||||
self.name_changed(symbol.address, symbol.name)
|
||||
|
||||
if removed:
|
||||
self.name_changed(symbol.address, "sub_%x" % symbol.address)
|
||||
else:
|
||||
self.name_changed(symbol.address, symbol.name)
|
||||
|
||||
def name_changed(self, address, name):
|
||||
"""
|
||||
|
||||
@@ -150,14 +150,21 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
self._dockable_factory[dockable_name] = create_widget_callback
|
||||
|
||||
def create_dockable_widget(self, parent, dockable_name):
|
||||
import sip
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if USING_PYSIDE6:
|
||||
from shiboken6 import wrapInstance
|
||||
else:
|
||||
import sip
|
||||
def wrapInstance(ptr, base=None):
|
||||
return sip.wrapinstance(int(ptr), base)
|
||||
|
||||
# create a dockable widget, and save a reference to it for later use
|
||||
twidget = idaapi.create_empty_widget(dockable_name)
|
||||
self._dockable_widgets[dockable_name] = twidget
|
||||
|
||||
# cast the IDA 'twidget' as a Qt widget for use
|
||||
widget = sip.wrapinstance(int(twidget), QtWidgets.QWidget)
|
||||
widget = wrapInstance(int(twidget), QtWidgets.QWidget)
|
||||
widget.name = dockable_name
|
||||
widget.visible = False
|
||||
|
||||
@@ -226,7 +233,12 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
# attempt to generate an 'html' dump of the first 0x20 bytes (instructions)
|
||||
ida_fd = idaapi.fopenWT(path)
|
||||
idaapi.gen_file(idaapi.OFILE_LST, ida_fd, imagebase, imagebase+0x20, idaapi.GENFLG_GENHTML)
|
||||
idaapi.eclose(ida_fd)
|
||||
# IDA 9.x SDK fix: removed `idaapi.eclose`, added `ida_fpro.qfclose`
|
||||
if int(idaapi.get_kernel_version()[0]) >= 9:
|
||||
import ida_fpro
|
||||
ida_fpro.qfclose(ida_fd)
|
||||
else:
|
||||
idaapi.eclose(ida_fd)
|
||||
|
||||
# read the dumped text
|
||||
with open(path, "r") as fd:
|
||||
@@ -290,9 +302,16 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
# touch the target form so we know it is populated
|
||||
self._touch_ida_window(twidget)
|
||||
|
||||
# locate the Qt Widget for a form and take 1px image slice of it
|
||||
import sip
|
||||
widget = sip.wrapinstance(int(twidget), QtWidgets.QWidget)
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if USING_PYSIDE6:
|
||||
from shiboken6 import wrapInstance
|
||||
else:
|
||||
import sip
|
||||
def wrapInstance(ptr, base=None):
|
||||
return sip.wrapinstance(int(ptr), base)
|
||||
|
||||
widget = wrapInstance(int(twidget), QtWidgets.QWidget)
|
||||
pixmap = widget.grab(QtCore.QRect(0, 10, widget.width(), 1))
|
||||
|
||||
# convert the raw pixmap into an image (easier to interface with)
|
||||
@@ -592,4 +611,3 @@ def lex_citem_indexes(line):
|
||||
|
||||
# return all the citem indexes extracted from this line of text
|
||||
return indexes
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ def register_callback(callback_list, callback):
|
||||
|
||||
# 'register' the callback
|
||||
callback_list.append(callback_ref)
|
||||
return callback_ref
|
||||
|
||||
def notify_callback(callback_list, *args):
|
||||
"""
|
||||
@@ -208,32 +209,12 @@ def notify_callback(callback_list, *args):
|
||||
for callback_ref in cleanup:
|
||||
callback_list.remove(callback_ref)
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Coverage Util
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
def build_hitmap(data):
|
||||
def unregister_callback(callback_list, callback_ref):
|
||||
"""
|
||||
Build a hitmap from the given list of address.
|
||||
|
||||
A hitmap is a map of address --> number of executions.
|
||||
|
||||
The list of input addresses can be any sort of runtime trace, coverage,
|
||||
or profiling data that one would like to build a hitmap for.
|
||||
Remove a previously-registered callback reference.
|
||||
"""
|
||||
output = collections.defaultdict(int)
|
||||
|
||||
# if there is no input data, simply return an empty hitmap
|
||||
if not data:
|
||||
return output
|
||||
|
||||
#
|
||||
# walk through the given list of given addresses and build a
|
||||
# corresponding hitmap for them
|
||||
#
|
||||
|
||||
for address in data:
|
||||
output[address] += 1
|
||||
|
||||
# return the hitmap
|
||||
return output
|
||||
try:
|
||||
callback_list.remove(callback_ref)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
@@ -29,11 +29,16 @@ USING_PYSIDE6 = False
|
||||
# specific dependencies in here...
|
||||
#
|
||||
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
try:
|
||||
import ida_idaapi
|
||||
USING_IDA = True
|
||||
import idaapi
|
||||
ver_major, ver_minor = map(int, idaapi.get_kernel_version().split("."))
|
||||
USING_NEW_IDA = ver_major == 9 and ver_minor >= 2
|
||||
USING_OLD_IDA = not(USING_NEW_IDA)
|
||||
except ImportError:
|
||||
USING_IDA = False
|
||||
USING_NEW_IDA = False
|
||||
USING_OLD_IDA = False
|
||||
|
||||
try:
|
||||
import binaryninjaui
|
||||
@@ -47,8 +52,8 @@ except ImportError:
|
||||
# PyQt5 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# attempt to load PyQt5 (IDA 7.0+)
|
||||
if USING_IDA:
|
||||
# attempt to load PyQt5 (IDA from 7.0 to 9.1)
|
||||
if USING_OLD_IDA:
|
||||
|
||||
try:
|
||||
import PyQt5.QtGui as QtGui
|
||||
@@ -91,8 +96,8 @@ if not QT_AVAILABLE and USING_OLD_BINJA:
|
||||
# PySide6 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# If all else fails, try to load PySide6 (New Binary Ninja)
|
||||
if not QT_AVAILABLE and USING_NEW_BINJA:
|
||||
# If all else fails, try to load PySide6 (New Binary Ninja and IDA)
|
||||
if not QT_AVAILABLE and (USING_NEW_BINJA or USING_NEW_IDA):
|
||||
|
||||
try:
|
||||
import PySide6.QtGui as QtGui
|
||||
|
||||
@@ -37,8 +37,8 @@ def copy_to_clipboard(data):
|
||||
Copy the given data (a string) to the system clipboard.
|
||||
"""
|
||||
cb = QtWidgets.QApplication.clipboard()
|
||||
cb.clear(mode=cb.Clipboard)
|
||||
cb.setText(data, mode=cb.Clipboard)
|
||||
cb.clear(mode=QtGui.QClipboard.Mode.Clipboard)
|
||||
cb.setText(data, mode=QtGui.QClipboard.Mode.Clipboard)
|
||||
|
||||
def flush_qt_events():
|
||||
"""
|
||||
@@ -71,17 +71,17 @@ def get_dpi_scale():
|
||||
# xHeight is expected to be 40.0 at normal DPI
|
||||
return fm.height() / 173.0
|
||||
|
||||
def compute_color_on_gradiant(percent, color1, color2):
|
||||
def compute_color_on_gradient(percent, color1, color2):
|
||||
"""
|
||||
Compute the color specified by a percent between two colors.
|
||||
"""
|
||||
r1, g1, b1, _ = color1.getRgb()
|
||||
r2, g2, b2, _ = color2.getRgb()
|
||||
|
||||
# compute the new color across the gradiant of color1 -> color 2
|
||||
r = r1 + percent * (r2 - r1)
|
||||
g = g1 + percent * (g2 - g1)
|
||||
b = b1 + percent * (b2 - b1)
|
||||
# compute the new color across the gradient of color1 -> color 2
|
||||
r = r1 + int(percent * (r2 - r1))
|
||||
g = g1 + int(percent * (g2 - g1))
|
||||
b = b1 + int(percent * (b2 - b1))
|
||||
|
||||
# return the new color
|
||||
return QtGui.QColor(r,g,b)
|
||||
@@ -121,8 +121,8 @@ def prompt_string(label, title, default=""):
|
||||
dlg.setWindowTitle(title)
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(
|
||||
dpi_scale*400,
|
||||
dpi_scale*50
|
||||
int(dpi_scale*400),
|
||||
int(dpi_scale*50)
|
||||
)
|
||||
dlg.setModal(True)
|
||||
dlg.show()
|
||||
|
||||
@@ -62,7 +62,7 @@ class WaitBox(QtWidgets.QDialog):
|
||||
# configure the main widget / form
|
||||
self.setSizeGripEnabled(False)
|
||||
self.setModal(True)
|
||||
self._dpi_scale = get_dpi_scale()*5.0
|
||||
self._dpi_scale = get_dpi_scale()*5
|
||||
|
||||
# initialize abort button
|
||||
self._abort_button = QtWidgets.QPushButton("Cancel")
|
||||
@@ -83,19 +83,19 @@ class WaitBox(QtWidgets.QDialog):
|
||||
v_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||
v_layout.addWidget(self._text_label)
|
||||
if self._abort:
|
||||
self._abort_button.clicked.connect(abort)
|
||||
self._abort_button.clicked.connect(self._abort)
|
||||
v_layout.addWidget(self._abort_button)
|
||||
|
||||
v_layout.setSpacing(self._dpi_scale*3)
|
||||
v_layout.setSpacing(int(self._dpi_scale*3))
|
||||
v_layout.setContentsMargins(
|
||||
self._dpi_scale*5,
|
||||
self._dpi_scale,
|
||||
self._dpi_scale*5,
|
||||
self._dpi_scale
|
||||
int(self._dpi_scale*5),
|
||||
int(self._dpi_scale),
|
||||
int(self._dpi_scale*5),
|
||||
int(self._dpi_scale)
|
||||
)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = self._dpi_scale * 15
|
||||
height = int(self._dpi_scale * 15)
|
||||
self.setMinimumHeight(height)
|
||||
|
||||
# compute the dialog layout
|
||||
|
||||
@@ -23,7 +23,7 @@ def check_for_update(current_version, callback):
|
||||
update_thread = threading.Thread(
|
||||
target=async_update_check,
|
||||
args=(current_version, callback,),
|
||||
name="UpdateChecker"
|
||||
name="Lighthouse UpdateChecker"
|
||||
)
|
||||
update_thread.start()
|
||||
|
||||
@@ -32,6 +32,7 @@ def async_update_check(current_version, callback):
|
||||
An async worker thread to check for an plugin update.
|
||||
"""
|
||||
logger.debug("Checking for update...")
|
||||
current_version = "v" + current_version
|
||||
|
||||
try:
|
||||
response = urlopen(UPDATE_URL, timeout=5.0)
|
||||
@@ -42,7 +43,7 @@ def async_update_check(current_version, callback):
|
||||
logger.debug(" - Failed to reach GitHub for update check...")
|
||||
return
|
||||
|
||||
# convert vesrion #'s to integer for easy compare...
|
||||
# convert version #'s to integer for easy compare...
|
||||
version_remote = int(''.join(re.findall('\d+', remote_version)))
|
||||
version_local = int(''.join(re.findall('\d+', current_version)))
|
||||
|
||||
@@ -54,8 +55,8 @@ def async_update_check(current_version, callback):
|
||||
|
||||
# notify the user if an update is available
|
||||
update_message = "An update is available for Lighthouse!\n\n" \
|
||||
" - Latest Version: %s\n" % (remote_version) + \
|
||||
" - Current Version: %s\n\n" % (current_version) + \
|
||||
" - Latest Version: %s\n" % (remote_version) + \
|
||||
" - Current Version: %s\n\n" % (current_version) + \
|
||||
"Please go download the update from GitHub."
|
||||
|
||||
callback(update_message)
|
||||
|
||||
Reference in New Issue
Block a user