Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ scripts/perf/*.zip
*/.DS_Store
Pipfile
Pipfile.lock
uv.lock
/cache/
.github/binja/binaryninja
.github/binja/download_headless.py
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

### capa Explorer IDA Pro plugin

- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707

### Development

- ci: remove redundant "test_run" action from build workflow @mike-hunhoff #2692
Expand Down
2 changes: 1 addition & 1 deletion capa/ida/plugin/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@


import ida_kernwin
from PyQt5 import QtCore

from capa.ida.plugin.error import UserCancelledError
from capa.ida.plugin.qt_compat import QtCore
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
from capa.features.extractors.base_extractor import FunctionHandle

Expand Down
2 changes: 1 addition & 1 deletion capa/ida/plugin/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import idaapi
import ida_kernwin
import ida_settings
from PyQt5 import QtGui, QtCore, QtWidgets

import capa.main
import capa.rules
Expand Down Expand Up @@ -51,6 +50,7 @@
from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
from capa.features.extractors.base_extractor import FunctionHandle

logger = logging.getLogger(__name__)
Expand Down
4 changes: 2 additions & 2 deletions capa/ida/plugin/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@

import idc
import idaapi
from PyQt5 import QtCore

import capa.ida.helpers
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtCore, qt_get_item_flag_tristate


def info_to_name(display):
Expand Down Expand Up @@ -55,7 +55,7 @@ def __init__(self, parent: Optional["CapaExplorerDataItem"], data: list[str], ca
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

if self._can_check:
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | qt_get_item_flag_tristate()

if self.pred:
self.pred.appendChild(self)
Expand Down
2 changes: 1 addition & 1 deletion capa/ida/plugin/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import idc
import idaapi
from PyQt5 import QtGui, QtCore

import capa.rules
import capa.ida.helpers
Expand All @@ -42,6 +41,7 @@
CapaExplorerInstructionViewItem,
)
from capa.features.address import Address, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtGui, QtCore

# default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xE6C700
Expand Down
4 changes: 1 addition & 3 deletions capa/ida/plugin/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from PyQt5 import QtCore
from PyQt5.QtCore import Qt

from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import Qt, QtCore


class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
Expand Down
72 changes: 72 additions & 0 deletions capa/ida/plugin/qt_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Qt compatibility layer for capa IDA Pro plugin.

Handles PyQt5 (IDA < 9.2) vs PySide6 (IDA >= 9.2) differences.
This module provides a unified import interface for Qt modules and handles
API changes between Qt5 and Qt6.
"""

try:
# IDA 9.2+ uses PySide6
from PySide6 import QtGui, QtCore, QtWidgets # noqa: F401

QT_LIBRARY = "PySide6"
except ImportError:
# Older IDA versions use PyQt5
try:
from PyQt5 import QtGui, QtCore, QtWidgets # noqa: F401

QT_LIBRARY = "PyQt5"
except ImportError:
raise ImportError("Neither PySide6 nor PyQt5 is available. Cannot initialize capa IDA plugin.")

Qt = QtCore.Qt


def qt_get_item_flag_tristate():
"""
Get the tristate item flag compatible with Qt5 and Qt6.

Qt5 (PyQt5): Uses Qt.ItemIsTristate
Qt6 (PySide6): Qt.ItemIsTristate was removed, uses Qt.ItemIsAutoTristate

ItemIsAutoTristate automatically manages tristate based on child checkboxes,
matching the original ItemIsTristate behavior where parent checkboxes reflect
the check state of their children.

Returns:
int: The appropriate flag value for the Qt version

Raises:
AttributeError: If the tristate flag cannot be found in the Qt library
"""
if QT_LIBRARY == "PySide6":
# Qt6: ItemIsTristate was removed, replaced with ItemIsAutoTristate
# Try different possible locations (API varies slightly across PySide6 versions)
if hasattr(Qt, "ItemIsAutoTristate"):
return Qt.ItemIsAutoTristate
elif hasattr(Qt, "ItemFlag") and hasattr(Qt.ItemFlag, "ItemIsAutoTristate"):
return Qt.ItemFlag.ItemIsAutoTristate
else:
raise AttributeError(
"Cannot find ItemIsAutoTristate in PySide6. "
+ "Your PySide6 version may be incompatible with capa. "
+ f"Available Qt attributes: {[attr for attr in dir(Qt) if 'Item' in attr]}"
)
else:
# Qt5: Use the original ItemIsTristate flag
return Qt.ItemIsTristate
2 changes: 1 addition & 1 deletion capa/ida/plugin/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import idc
import idaapi
from PyQt5 import QtGui, QtCore, QtWidgets

import capa.rules
import capa.engine
Expand All @@ -28,6 +27,7 @@
from capa.ida.plugin.item import CapaExplorerFunctionItem
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets

MAX_SECTION_SIZE = 750

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ known_first_party = [
"idc",
"java",
"netnode",
"PyQt5"
"PyQt5",
"PySide6"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also should bump ida-settings>=3.0.0?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, ida-settings has some breaking changes that won't work here yet. so we need to pin to less than 3

]

[tool.deptry.per_rule_ignores]
Expand Down