Compare commits

...

53 Commits

Author SHA1 Message Date
gaasedelen 0350b0c538 Follow-up to #157: unsubscribe torn-down coverage overview refresh callback 2026-02-13 19:39:44 -05:00
gaasedelen 88051b0652 Follow-up to #157: keep IDA8/9.1 sip path and bump dev version to 0.9.4 2026-02-13 18:56:11 -05:00
gaasedelen 1d7e4b321c Merge PR #157: IDA 9.2/9.3 compatibility fixes 2026-02-13 18:55:54 -05:00
raptor 602bb611a4 Fix some issues in IDA 9.1 2025-09-09 14:04:28 +02:00
raptor c5cc03d7dc Improve IDA 9.2 support without breaking previous supported versions 2025-09-09 13:34:47 +02:00
raptor 720b036a9d Start working on IDA 9.2 port 2025-09-09 11:43:22 +02:00
raptor b8ff268c86 IDA 9.x SDK fix 2025-08-28 17:47:43 +02:00
gaasedelen 88ceac7b19 tweak to make lighthouse copy *all* in-box theme files to the user theme directory... 2024-02-06 13:59:33 -05:00
gaasedelen 9a56463103 make update dialog look a little less janky 2024-02-06 13:30:54 -05:00
gaasedelen 562595be9b Merge branch 'develop' 2024-02-05 13:05:19 -05:00
gaasedelen eeb16a5f81 final fixes, ticking version numbers 2024-02-05 13:03:41 -05:00
gaasedelen 84eeb210c6 minor cleanup, finalizing for release 2024-02-02 19:28:55 -05:00
gaasedelen f944b62cc3 Update Intel pin build scripts for pin-3.30-98830 (MSVC) 2024-02-02 19:06:33 -05:00
gaasedelen 1b883377d6 switch database metadata instruction listing to a set, dramatically improving coverage loading perf in several cases, fixing #128 2024-02-02 17:31:56 -05:00
gaasedelen 146eb4fd2d fix bug where 'X' column in combobox was super wide/fat under PySide6 for some reason 2024-02-02 17:03:08 -05:00
gaasedelen de2704b680 Coverage Xref right click UI action rewrite for binja 2024-02-02 16:38:04 -05:00
gaasedelen 96df2c5a1f adds "long night" theme by ioncodes 2024-02-02 12:19:31 -05:00
gaasedelen 9c579c6e9d fix #131, hopefully improves dpi issues cross platform 2024-02-02 11:55:05 -05:00
gaasedelen ee7ac30f14 fix binary ninja "suspicious" warnings 2024-02-02 11:32:04 -05:00
gaasedelen 5f8fa02de2 fix function/symbol rename event hooks in binja 2024-02-02 11:32:04 -05:00
Thomas Dupuy 87defff5cd FIx small typo. 2024-02-01 20:31:24 -05:00
gaasedelen e1438159da fixes coverage xref compatability issues (timestamp crash) with binja / PySide6 per #123 2024-02-01 20:25:08 -05:00
gaasedelen 856768d129 fix clipboard compat issue exposed by PySide6 2024-02-01 20:08:26 -05:00
gaasedelen 7d67e41773 Fixes issue raised in #129 while retaining compatability and functionality across IDA / binja. 2024-02-01 19:54:30 -05:00
gaasedelen 8f3c23bbb6 maintain compatability with IDA 2024-02-01 18:50:58 -05:00
Alexandre Brenner 4992d4b731 Fix ValueError
Only tested on Windows
2024-02-01 18:50:58 -05:00
Alexandre Brenner 550c476e81 Fix TypeError
`TypeError: BasicBlock._create_instance() takes 2 positional arguments but 3 were given`
Only tested on Windows
2024-02-01 18:50:58 -05:00
mishap mishap bba8d91253 Allow partial module whitelist match 2024-01-18 20:49:25 -05:00
gaasedelen 0e0e08e564 normalize code/comments of drcov 3 fix 2024-01-18 19:47:48 -05:00
PavelBlinnikov 6912018b89 add support for drcov version 3 2024-01-18 19:47:48 -05:00
Moritz Schloegel f4642e8b4b fix typo in gradient 2022-02-17 14:29:27 -05:00
Moritz Schloegel 6e1dcb8d40 convert float to int as expected by Qt API functions (fixes #116) 2022-02-17 14:29:27 -05:00
Alexander Taylor 973b40e6d0 Fix for func possibly being None. 2022-01-10 11:36:32 -05:00
gaasedelen e3d636a3f4 adds support for tracking, viewing, and dumping orphan coverage (#108) 2021-10-05 22:48:37 -04:00
gaasedelen 7245a2d2c4 Merge branch 'develop' 2021-08-10 16:41:04 -04:00
gaasedelen 701952d83f add x86 supprt to tenet reader 2021-08-10 16:39:05 -04:00
gaasedelen 9ed0ccf528 updated readme, version number 2021-08-10 16:35:18 -04:00
gaasedelen ae1072a04b improves background color detection logic 2021-08-09 16:43:18 -04:00
gaasedelen 4c19fee630 bugfix when loading executables with unicode names on Python 2.7 2021-08-09 16:42:47 -04:00
gaasedelen d02fbf13d9 adds support for loading text-based Tenet traces 2021-08-06 18:45:51 -04:00
_yrp 19eddd6470 Lower block trace confidence (#105)
Depending on how blocks are traced, on some targets 0.9 is too high. Specifically, some emulator based tracers might consider the first instruction after a return to be a new "block" -- this will inflate the number of instructions with coverage path the threshold, even though the trace should still be considered block cov.
2021-08-06 18:41:41 -04:00
gaasedelen 09a95061ed reduce banner print 2021-08-03 16:47:34 -04:00
gaasedelen 94759790de font tweaks again 2021-08-03 16:46:56 -04:00
gaasedelen 374ca999f8 maybe fixes qt conflict issues? (issue #100) 2021-08-03 12:31:10 -04:00
gaasedelen b3c8683110 font fixes for macos...? for the 10th time...? 2021-07-30 09:03:40 -04:00
gaasedelen aa71810bf7 PySide6 & Python2 Compat :-X 2021-07-30 09:03:11 -04:00
gaasedelen b0f798a3d9 a bit of cleanup for binja plugin manager stuff 2021-07-29 10:42:45 -04:00
gaasedelen 85b08a9611 spindown lighthouse contexts in binja 2021-07-29 10:33:00 -04:00
gaasedelen f2031f921f a bit of housekeeping on drcov.py 2021-07-29 06:08:14 -04:00
Jordan 8e98a74eee Qt6 Updates for Binja + Binja plugin manager compatability (#106)
* initial pyside6 support and testing binary ninja plugin installation

* small tweak to fix plugin working with internal test builds
2021-07-29 05:47:36 -04:00
gaasedelen 85818f5b9b Merge pull request #98 from angelystor/master
Added DrCov module table version 5 support
2021-04-09 19:41:08 -04:00
Angelystor 8434c80e18 Added DrCov module table version 5 support which the latest DynamoRIO outputs 2021-04-08 18:55:09 +08:00
DrWhax a25e77764a Fixes for fixing the PIN build for all operating systems (#94)
* remove unneeded std namespace

* remove Wno-aligned-new

* This makes the build work again, tested on VC2015 community edition on
Windows Server 2019.

Credit for these changes go to: https://github.com/HongThatCong/Lighthouse-CodeCoverage-build
2020-12-26 21:57:41 -05:00
41 changed files with 1069 additions and 434 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2020 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
+22 -10
View File
@@ -1,13 +1,14 @@
# Lighthouse - A Code Coverage Explorer for Reverse Engineers
# Lighthouse - A Coverage Explorer for Reverse Engineers
<p align="center">
<img alt="Lighthouse Plugin" src="screenshots/overview.gif"/>
</p>
## Overview
Lighthouse is a powerful code coverage plugin for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/). As an extension of the leading disassemblers, this plugin enables one to interactively explore code coverage data in new and innovative ways when symbols or source may not be available for a given binary.
Lighthouse is a powerful code coverage explorer for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/), providing software researchers with uniquely interactive controls to study execution maps for native applications without requiring symbols or source.
This plugin is labeled only as a prototype & code resource for the community.
This project placed 2nd in IDA's [2017 Plug-In Contest](https://hex-rays.com/contests_details/contest2017/) and was later [nominated](https://pwnies.com/lighthouse/) in the 2021 Pwnie Awards for its contributions to the security research industry.
Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
@@ -27,11 +28,22 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
Lighthouse is a cross-platform (Windows, macOS, Linux) Python 2/3 plugin. It takes zero third party dependencies, making the code both portable and easy to install.
1. From your disassembler's python console, run the following command to find its plugin directory:
- **IDA Pro**: `os.path.join(idaapi.get_user_idadir(), "plugins")`
- **Binary Ninja**: `binaryninja.user_plugin_path()`
Use the instructions below for your respective disassembler.
2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
## IDA Installation
1. From IDA's Python console, run the following command to find its plugin directory:
- `import idaapi, os; print(os.path.join(idaapi.get_user_idadir(), "plugins"))`
2. Copy the contents of this repository's `/plugins/` folder to the listed directory.
3. Restart your disassembler.
## Binary Ninja Installation
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.
# Usage
@@ -76,7 +88,7 @@ If there are any other actions that you think might be useful to add to this con
## Coverage ComboBox
Loaded coverage data and user constructed compositions can be selected or deleted through the coverage combobox.
Loaded coverage and user constructed compositions can be selected or deleted through the coverage combobox.
<p align="center">
<img alt="Lighthouse Coverage ComboBox" src="screenshots/combobox.gif"/>
@@ -84,8 +96,7 @@ Loaded coverage data and user constructed compositions can be selected or delete
## HTML Coverage Report
Lighthouse can generate a rudimentary HTML coverage report of the active coverage.
A sample report can be seen [here](https://rawgit.com/gaasedelen/lighthouse/master/testcase/report.html).
Lighthouse can generate rudimentary HTML coverage reports. A sample report can be seen [here](https://rawgit.com/gaasedelen/lighthouse/master/testcase/report.html).
<p align="center">
<img alt="Lighthouse HTML Report" src="screenshots/html_report.gif"/>
@@ -183,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
+11
View File
@@ -0,0 +1,11 @@
# Lighthouse - A Coverage Explorer for Reverse Engineers
<p align="center">
<img alt="Lighthouse Plugin" src="https://raw.githubusercontent.com/gaasedelen/lighthouse/master/screenshots/overview.gif"/>
</p>
## Overview
Lighthouse is a powerful code coverage explorer for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/), providing software researchers with uniquely interactive controls to study execution maps for native applications without requiring symbols or source.
For additional usage information, please check out the full [README](https://github.com/gaasedelen/lighthouse) on GitHub.
+26
View File
@@ -0,0 +1,26 @@
import os
import sys
#------------------------------------------------------------------------------
# Binary Ninja 'Plugin Manager' Stub
#------------------------------------------------------------------------------
#
# This file is an alternative loading stub created specifically to
# support the ability to 'easy' install Lighthouse into Binary Ninja
# via its 'Plugin Manager' functionality.
#
# Please disregard this code / subdirectory if performing **manual**
# installations of Lighthouse in IDA or Binary Ninja.
#
lh_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")
sys.path.append(lh_path)
from lighthouse.util.log import logging_started, start_logging
from lighthouse.util.disassembler import disassembler
if not logging_started():
logger = start_logging()
logger.info("Selecting Binary Ninja loader...")
from lighthouse.integration.binja_loader import *
+24
View File
@@ -0,0 +1,24 @@
{
"api": [
"python3"
],
"author": "Markus Gaasedelen",
"description": "A Coverage Explorer for Reverse Engineers",
"license": {
"name": "MIT",
"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": 4526,
"name": "Lighthouse",
"platforms": [
"Darwin",
"Linux",
"Windows"
],
"pluginmetadataversion": 2,
"type": [
"helper"
],
"version": "0.9.4"
}
+1 -1
View File
@@ -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
-1
View File
@@ -1,4 +1,3 @@
using namespace std;
#include <iostream>
#include <set>
#include <string>
-1
View File
@@ -1,4 +1,3 @@
using namespace std;
#include "ImageManager.h"
#include "pin.H"
+1 -1
View File
@@ -1,7 +1,7 @@
CONFIG_ROOT := $(PIN_ROOT)/source/tools/Config
include $(CONFIG_ROOT)/makefile.config
TOOL_CXXFLAGS += -std=c++11 -Wno-format -Wno-aligned-new
TOOL_CXXFLAGS += -std=c++11 -Wno-format
TOOL_ROOTS := CodeCoverage
$(OBJDIR)CodeCoverage$(PINTOOL_SUFFIX): $(OBJDIR)CodeCoverage$(OBJ_SUFFIX) $(OBJDIR)ImageManager$(OBJ_SUFFIX)
+13 -5
View File
@@ -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
+4 -6
View File
@@ -2,11 +2,12 @@
cls
cl ^
/c ^
/c /Fo /nologo /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MD /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /wd4316 /wd4530 /fp:strict ^
/DTARGET_IA32E /DHOST_IA32E /DTARGET_WINDOWS /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D_STLP_IMPORT_IOSTREAMS /D__LP64__ ^
/I"%PIN_ROOT%\extras\xed-intel64\include\xed" ^
/I%PIN_ROOT%\source\include\pin ^
/I%PIN_ROOT%\source\include\pin\gen ^
/I%PIN_ROOT%\source\tools\InstLib ^
/I"%PIN_ROOT%\extras\xed-intel64\include\xed" ^
/I%PIN_ROOT%\extras\components\include ^
/I%PIN_ROOT%\extras\stlport\include ^
/I%PIN_ROOT%\extras ^
@@ -16,9 +17,6 @@ cl ^
/I"%PIN_ROOT%\extras\crt\include\arch-x86_64" ^
/I%PIN_ROOT%\extras\crt\include\kernel\uapi ^
/I"%PIN_ROOT%\extras\crt\include\kernel\uapi\asm-x86" ^
/nologo /W3 /WX- /O2 ^
/D TARGET_IA32E /D HOST_IA32E /D TARGET_WINDOWS /D WIN32 /D __PIN__=1 /D PIN_CRT=1 /D __LP64__ ^
/Gm- /MT /GS- /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR- /Gd /TP /wd4530 /GR- /GS- /EHs- /EHa- /FP:strict /Oi- ^
/FIinclude/msvc_compat.h CodeCoverage.cpp ImageManager.cpp ImageManager.h TraceFile.h
link ^
@@ -29,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 kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-64.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 ^
+3 -3
View File
@@ -2,8 +2,8 @@
cls
cl ^
/c /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MT /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /fp:precise /nologo /wd4316 ^
/DTARGET_IA32 /DHOST_IA32 /DTARGET_WINDOWS /DBIGARRAY_MULTIPLIER=1 /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D__i386__ ^
/c /Fo /nologo /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MD /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /wd4316 /wd4530 /fp:precise ^
/DTARGET_IA32 /DHOST_IA32 /DTARGET_WINDOWS /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D_STLP_IMPORT_IOSTREAMS /D__i386__ ^
/I"%PIN_ROOT%\extras\xed-ia32\include\xed" ^
/I%PIN_ROOT%\source\include\pin ^
/I%PIN_ROOT%\source\include\pin\gen ^
@@ -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 kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-32.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 ^
+1 -1
View File
@@ -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
View File
@@ -66,6 +66,8 @@ class LighthouseContext(object):
"""
Spin down any session subsystems before the session is deleted.
"""
if not self._started:
return
self.painter.terminate()
self.director.terminate()
self.metadata.terminate()
+98 -49
View File
@@ -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
+17 -94
View File
@@ -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.90
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
#----------------------------------------------------------------------
@@ -25,6 +25,8 @@ class LighthouseBinja(LighthouseCore):
def get_context(self, dctx, startup=True):
"""
Get the LighthouseContext object for a given database context.
In Binary Ninja, a dctx is a BinaryView (BV).
"""
dctx_id = ctypes.addressof(dctx.handle.contents)
@@ -54,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!
#
@@ -65,21 +67,52 @@ class LighthouseBinja(LighthouseCore):
# return the lighthouse context object for this database ctx / bv
return lctx
def binja_close_context(self, dctx):
"""
Attempt to close / spin-down the LighthouseContext for the given dctx.
In Binary Ninja, a dctx is a BinaryView (BV).
"""
dctx_id = ctypes.addressof(dctx.handle.contents)
# fetch the LighthouseContext for the closing BNDB
try:
lctx = self.lighthouse_contexts.pop(dctx_id)
#
# if lighthouse was not actually used for this BNDB / session, then
# the lookup will fail as there is nothing to spindown
#
except KeyError:
return
# spin down the closing context (stop threads, cleanup qt state, etc)
logger.info("Closing a LighthouseContext...")
lctx.terminate()
#--------------------------------------------------------------------------
# UI Integration (Internal)
#--------------------------------------------------------------------------
#
# 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()
@@ -95,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):
@@ -126,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
#--------------------------------------------------------------------------
@@ -139,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...
@@ -30,4 +30,3 @@ try:
except Exception as e:
lmsg("Failed to initialize Lighthouse")
logger.exception("Exception details:")
+8 -12
View File
@@ -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.1"
PLUGIN_VERSION = "0.9.4-DEV"
AUTHORS = "Markus Gaasedelen"
DATE = "2020"
DATE = "2024"
#--------------------------------------------------------------------------
# Initialization
@@ -87,14 +86,10 @@ class LighthouseCore(object):
# build the main banner title
banner_params = (self.PLUGIN_VERSION, self.AUTHORS, self.DATE)
banner_title = "Lighthouse v%s - (c) %s - %s" % banner_params
banner_title = "v%s - (c) %s - %s" % banner_params
# print plugin banner
lmsg("")
lmsg("-"*75)
lmsg("---[ %s" % banner_title)
lmsg("-"*75)
lmsg("")
lmsg("Loaded %s" % banner_title)
#--------------------------------------------------------------------------
# Disassembler / Database Context Selector
@@ -195,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):
+1 -1
View File
@@ -88,7 +88,7 @@ class LighthouseIDAPlugin(idaapi.plugin_t):
except Exception as e:
logger.exception("Failed to cleanly unload Lighthouse from IDA.")
end = time.time()
print("-"*50)
logger.debug("-"*50)
logger.debug("IDA term done... (%.3f seconds...)" % (end-start))
+61 -28
View File
@@ -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
+1 -1
View File
@@ -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
-2
View File
@@ -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):
+66 -16
View File
@@ -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):
"""
@@ -192,7 +207,7 @@ class DrcovData(CoverageFile):
data_name, version = version_data.split(" ")
#assert data_name == "version"
self.module_table_version = int(version)
if not self.module_table_version in [2, 3, 4]:
if not self.module_table_version in [2, 3, 4, 5]:
raise ValueError("Unsupported (new?) drcov log format...")
# parse module count in table from 'count Y'
@@ -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...
@@ -310,22 +331,30 @@ class DrcovData(CoverageFile):
# parse the plaintext basic block entries one by one
else:
self._parse_bb_table_text_entries(f)
def _parse_bb_table_text_entries(self, f):
"""
Parse drcov log basic block table text entries from filestream.
"""
table_header = f.readline().decode('utf-8').strip()
if table_header != "module id, start, size:":
raise ValueError("Invalid BB header: %r" % table_header)
pattern = re.compile(r"^module\[\s*(?P<mod>[0-9]+)\]\:\s*(?P<start>0x[0-9a-fA-F]+)\,\s*(?P<size>[0-9]+)$")
for i, bb in enumerate(self.bbs):
text_entry = f.readline().decode('utf-8').strip()
if not text_entry:
continue
if text_entry != "module id, start, size:":
raise ValueError("Invalid BB header: %r" % text_entry)
match = pattern.match(text_entry)
if not match:
raise ValueError("Invalid BB entry: %r" % text_entry)
pattern = re.compile(r"^module\[\s*(?P<mod>[0-9]+)\]\:\s*(?P<start>0x[0-9a-fA-F]+)\,\s*(?P<size>[0-9]+)$")
for bb in self.bbs:
text_entry = f.readline().decode('utf-8').strip()
match = pattern.match(text_entry)
if not match:
raise ValueError("Invalid BB entry: %r" % text_entry)
bb.start = int(match.group("start"), 16)
bb.size = int(match.group("size"), 10)
bb.mod_id = int(match.group("mod"), 10)
bb.start = int(match.group("start"), 16)
bb.size = int(match.group("size"), 10)
bb.mod_id = int(match.group("mod"), 10)
#------------------------------------------------------------------------------
# drcov module parser
@@ -376,6 +405,8 @@ class DrcovModule(object):
self._parse_module_v3(data)
elif version == 4:
self._parse_module_v4(data)
elif version == 5:
self._parse_module_v5(data)
else:
raise ValueError("Unknown module format (v%u)" % version)
@@ -436,6 +467,25 @@ class DrcovModule(object):
self.size = self.end-self.base
self.filename = os.path.basename(self.path.replace('\\', os.sep))
def _parse_module_v5(self, data):
"""
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)
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))
#------------------------------------------------------------------------------
# drcov basic block parser
#------------------------------------------------------------------------------
@@ -0,0 +1,82 @@
import collections
from ..coverage_file import CoverageFile
# 'known' instruction pointer labels from Tenet traces
INSTRUCTION_POINTERS = ['EIP', 'RIP', 'PC']
class TenetData(CoverageFile):
"""
A Tenet trace log parser.
"""
def __init__(self, filepath):
self._hitmap = {}
super(TenetData, self).__init__(filepath)
#--------------------------------------------------------------------------
# Public
#--------------------------------------------------------------------------
def get_addresses(self, module_name=None):
return self._hitmap.keys()
#--------------------------------------------------------------------------
# Parsing Routines - Top Level
#--------------------------------------------------------------------------
def _parse(self):
"""
Parse absolute instruction addresses from the given Tenet trace.
"""
hitmap = collections.defaultdict(int)
with open(self.filepath) as f:
while True:
# read 128mb chunks of 'lines' from the file
lines = f.readlines(1024 * 1024 * 128)
# no more lines to process, break
if not lines:
break
# parse the instruction addresses from lines, into the hitmap
self._process_lines(lines, hitmap)
# save the hitmap if we completed parsing without crashing
self._hitmap = hitmap
def _process_lines(self, lines, hitmap):
"""
Parse instruction addresses out of the given text lines.
"""
for line in lines:
# split the line (an execution delta) into its individual entries
delta = line.split(",")
# process each item (a name=value pair) in the execution delta
for item in delta:
# split name/value pair, and normalize the name for matching
name, value = item.split("=")
name = name.upper()
# ignore entries that are not the instruction pointer
if not name in INSTRUCTION_POINTERS:
continue
# save the parsed instruction pointer address to the hitmap
address = int(value, 16)
hitmap[address] += 1
# break beacuse we don't expect two IP's on the same line
break
# continue to the next line
# ...
# done parsing this chunk of lines
return
+4 -5
View File
@@ -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)
+43 -4
View File
@@ -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()
@@ -275,6 +298,22 @@ class EventProxy(QtCore.QObject):
if int(event.type()) == self.EventDestroy:
source.removeEventFilter(self)
#
# XXX/V35: This is pretty hacky annoying stuff, but the lifetime
# of the CoverageOverview widget is managed internally by binja
# and gets deleted/cleaned up *after* a database is closed.
#
# it's best we just unload the lighthouse context in binja after
# the UI widgets have been destroyed (which aligns with IDA)
#
if disassembler.NAME == "BINJA":
lctx = self._target.lctx
core = lctx.core
core.binja_close_context(lctx.dctx)
# cleanup the UI / qt references for the CoverageOverview elements
self._target.terminate()
#
@@ -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)
#--------------------------------------------------------------------------
+203 -83
View File
@@ -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)
@@ -261,7 +265,12 @@ class CoverageTableView(QtWidgets.QTableView):
return
# show the popup menu to the user, and wait for their selection
action = ctx_menu.exec_(self.viewport().mapToGlobal(position))
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)
@@ -281,7 +290,11 @@ class CoverageTableView(QtWidgets.QTableView):
return
# show the popup menu to the user, and wait for their selection
action = ctx_menu.exec_(hh.viewport().mapToGlobal(position))
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)
@@ -298,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
@@ -311,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
@@ -376,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)
#--------------------------------------------------------------------------
@@ -429,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
@@ -527,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
#---------------------------------------------------------------------------
@@ -538,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
@@ -599,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:
@@ -627,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
#------------------------------------------------------------------------------
@@ -728,7 +803,9 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
# initialize a monospace font to use for table row / cell text
self._entry_font = MonospaceFont()
self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics)
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
@@ -824,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.
@@ -832,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
@@ -942,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
@@ -952,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
@@ -960,7 +1076,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
sorted_functions = sorted(
itervalues(self._visible_coverage),
key=attrgetter(sort_field),
reverse=sort_order
reverse=direction
)
#
@@ -978,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
#
@@ -994,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()
@@ -1305,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)}
+4 -4
View File
@@ -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)
+5 -5
View File
@@ -74,11 +74,11 @@ class ModuleSelector(QtWidgets.QDialog):
description_text = \
"Lighthouse could not automatically identify the target module in the given coverage file:<br />" \
"<br />" \
"-- <b>Target:</b> %s<br />" \
"-- <b>Coverage File:</b> %s<br />" \
"-- <b>Target:</b> {0}<br />" \
"-- <b>Coverage File:</b> {1}<br />" \
"<br />" \
"Please double click the name of the module that matches this database, or close this dialog<br />" \
"if you do not see your binary listed in the table below..." % (self._target_name, self._coverage_file)
"if you do not see your binary listed in the table below...".format(self._target_name, self._coverage_file)
self._label_description = QtWidgets.QLabel(description_text)
self._label_description.setTextFormat(QtCore.Qt.RichText)
@@ -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)
+13 -6
View File
@@ -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"
}
}
@@ -91,7 +91,7 @@ class BinjaCoreAPI(DisassemblerCoreAPI):
else: # commercial, personal
disassembler_version = version_string.split(" ", 1)[0]
major, minor, patch = map(int, disassembler_version.split("."))
major, minor, patch, *_= disassembler_version.split(".") + ['0']
# save the version number components for later use
self._version_major = major
@@ -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):
"""
@@ -380,7 +395,11 @@ if QT_AVAILABLE:
if not view_frame:
return False
import shiboken2 as shiboken
if USING_PYSIDE6:
import shiboken6 as shiboken
else:
import shiboken2 as shiboken
vf_ptr = shiboken.getCppPointer(view_frame)[0]
return self._visible_for_view[vf_ptr]
@@ -392,8 +411,13 @@ if QT_AVAILABLE:
self._active_view = None
return
import shiboken2 as shiboken
if USING_PYSIDE6:
import shiboken6 as shiboken
else:
import shiboken2 as shiboken
self._active_view = shiboken.getCppPointer(view_frame)[0]
if self.visible:
dock_handler = DockHandler.getActiveDockHandler()
dock_handler.setVisible(self.m_name, True)
+42 -10
View File
@@ -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:
@@ -238,16 +250,30 @@ class IDACoreAPI(DisassemblerCoreAPI):
except OSError:
pass
# attempt to parse the user's disassembly background color from the html
# attempt to parse the user's disassembly background color from the html (7.0?)
bg_color_text = get_string_between(html, '<body bgcolor="', '">')
if bg_color_text:
logger.debug(" - Extracted bgcolor '%s' from regex!" % bg_color_text)
return QtGui.QColor(bg_color_text)
# sometimes the above one isn't present... so try this one
#
# sometimes the above one isn't present... so try this one (7.1 - 7.4 maybe?)
#
# TODO: IDA 7.5 says c1 is /* line-fg-default */ ... but it's possible c1
# had the bg color of the line in other builds of 7.x? I'm not sure but
# this should be double checked at some point and can maybe just be removed
# in favor of c41 (line-bg-default) as that's what we really want
#
bg_color_text = get_string_between(html, '.c1 \{ background-color: ', ';')
if bg_color_text:
logger.debug(" - Extracted background-color '%s' from regex!" % bg_color_text)
logger.debug(" - Extracted background-color '%s' from line-fg-default!" % bg_color_text)
return QtGui.QColor(bg_color_text)
# -- IDA 7.5 says c41 is /* line-bg-default */, a.k.a the bg color for disassembly text
bg_color_text = get_string_between(html, '.c41 \{ background-color: ', ';')
if bg_color_text:
logger.debug(" - Extracted background-color '%s' from line-bg-default!" % bg_color_text)
return QtGui.QColor(bg_color_text)
logger.debug(" - HTML color regex failed...")
@@ -276,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)
@@ -578,4 +611,3 @@ def lex_citem_indexes(line):
# return all the citem indexes extracted from this line of text
return indexes
+7 -26
View File
@@ -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
+57 -5
View File
@@ -16,18 +16,45 @@ QT_AVAILABLE = False
#
# this file was critical for retaining compatibility with Qt4 frameworks
# used by IDA 6.8/6.95, but it less important now. support for Qt 4 and
# older versions of IDA will be deprecated in Lighthouse v0.9.0
# older versions of IDA (< 7.0) were deprecated in Lighthouse v0.9.0
#
USING_PYQT5 = False
USING_PYSIDE2 = False
USING_PYSIDE6 = False
#
# TODO/QT: This file is getting pretty gross. this whole shim system
# should probably get refactored as I really don't want disassembler
# 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 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_NEW_IDA = False
USING_OLD_IDA = False
try:
import binaryninjaui
USING_NEW_BINJA = "qt_major_version" in binaryninjaui.__dict__ and binaryninjaui.qt_major_version == 6
USING_OLD_BINJA = not(USING_NEW_BINJA)
except ImportError:
USING_NEW_BINJA = False
USING_OLD_BINJA = False
#------------------------------------------------------------------------------
# PyQt5 Compatibility
#------------------------------------------------------------------------------
# attempt to load PyQt5
if QT_AVAILABLE == False:
# attempt to load PyQt5 (IDA from 7.0 to 9.1)
if USING_OLD_IDA:
try:
import PyQt5.QtGui as QtGui
import PyQt5.QtCore as QtCore
@@ -45,8 +72,9 @@ if QT_AVAILABLE == False:
# PySide2 Compatibility
#------------------------------------------------------------------------------
# if PyQt5 did not import, try to load PySide
if QT_AVAILABLE == False:
# if PyQt5 did not import, try to load PySide2 (Old Binary Ninja / Cutter)
if not QT_AVAILABLE and USING_OLD_BINJA:
try:
import PySide2.QtGui as QtGui
import PySide2.QtCore as QtCore
@@ -64,3 +92,27 @@ if QT_AVAILABLE == False:
except ImportError:
pass
#------------------------------------------------------------------------------
# PySide6 Compatibility
#------------------------------------------------------------------------------
# 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
import PySide6.QtCore as QtCore
import PySide6.QtWidgets as QtWidgets
# alias for less PySide6 <--> PyQt5 shimming
QtCore.pyqtSignal = QtCore.Signal
QtCore.pyqtSlot = QtCore.Slot
QtWidgets.QAction = QtGui.QAction
# importing went okay, PySide must be available for use
QT_AVAILABLE = True
USING_PYSIDE6 = True
# import failed. No Qt / UI bindings available...
except ImportError:
pass
+10 -10
View File
@@ -19,7 +19,7 @@ def MonospaceFont():
Convenience alias for creating a monospace Qt font object.
"""
font = QtGui.QFont("Courier New")
font.setStyleHint(QtGui.QFont.TypeWriter)
font.setStyleHint(QtGui.QFont.Monospace)
return font
#------------------------------------------------------------------------------
@@ -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()
+8 -8
View File
@@ -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
+5 -4
View File
@@ -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)