diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d3c84ca..3fe31656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -397,7 +397,7 @@ jobs: python-version: 3.12.x output-id: artifact_windows - name: macos-x86_64 - os: macos-14 + os: macos-15-intel # Must be an x86_64 image: https://github.com/actions/runner-images#available-images arch: x86_64 package: true - name: macos-arm64 diff --git a/CMakeLists.txt b/CMakeLists.txt index d6289c88..e785237f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ set(CUTTER_EXTRA_PLUGIN_DIRS "" CACHE STRING "List of addition plugin locations" option(CUTTER_ENABLE_DEPENDENCY_DOWNLOADS "Enable downloading of dependencies. Setting to OFF doesn't affect any downloads done by rizin build." OFF) option(CUTTER_ENABLE_PACKAGING "Enable building platform-specific packages for distributing" OFF) option(CUTTER_ENABLE_SIGDB "Downloads and installs sigdb (only available when CUTTER_USE_BUNDLED_RIZIN=ON)." OFF) +option(CUTTER_ENABLE_TRANSLATIONS "Build and install translation files" ON) option(CUTTER_PACKAGE_DEPENDENCIES "During install step include the third party dependencies." OFF) option(CUTTER_PACKAGE_RZ_GHIDRA "Compile and install rz-ghidra during install step." OFF) option(CUTTER_PACKAGE_RZ_LIBSWIFT "Compile and install rz-libswift demangler during the install step." OFF) diff --git a/cmake/BundledRizin.cmake b/cmake/BundledRizin.cmake index b89399b4..dab0a44b 100644 --- a/cmake/BundledRizin.cmake +++ b/cmake/BundledRizin.cmake @@ -53,7 +53,7 @@ endif() # TODO: This version number should be fetched automatically # instead of being hardcoded. -set (Rizin_VERSION 0.9) +set (Rizin_VERSION 0.10) set (RZ_LIBS rz_core rz_config rz_cons rz_io rz_util rz_flag rz_mark rz_arch rz_debug rz_hash rz_bin rz_lang rz_il rz_egg rz_reg rz_search rz_syscall diff --git a/dist/CMakeLists.txt b/dist/CMakeLists.txt index 093d6a3a..d3102b6f 100644 --- a/dist/CMakeLists.txt +++ b/dist/CMakeLists.txt @@ -201,9 +201,9 @@ if(CUTTER_PACKAGE_RZ_GHIDRA) # installed Cutter. ExternalProject_Add(rz-ghidra GIT_REPOSITORY https://github.com/rizinorg/rz-ghidra - GIT_TAG v0.9.0 + #GIT_TAG v0.3.0 #GIT_TAG c7a50a2e7c0a95cd52b167c9ee0fa1805223f08e - #GIT_TAG dev + GIT_TAG dev #GIT_SHALLOW ON # disable this line when using commit hash CONFIGURE_COMMAND "" BUILD_COMMAND "" diff --git a/dist/bundle_jsdec.ps1 b/dist/bundle_jsdec.ps1 index e1b58bea..4eee6d98 100644 --- a/dist/bundle_jsdec.ps1 +++ b/dist/bundle_jsdec.ps1 @@ -4,7 +4,7 @@ $plugin_path = "$dist\plugins\native\" $pathdll = "$plugin_path\jsdec_cutter.dll" if (-not (Test-Path -Path 'jsdec' -PathType Container)) { - git clone https://github.com/rizinorg/jsdec.git --depth 1 --branch "v0.9.0" + git clone https://github.com/rizinorg/jsdec.git --depth 1 --branch "dev" } cd jsdec $jsdecdir = (Get-Item .).FullName diff --git a/dist/bundle_rz_libswift.ps1 b/dist/bundle_rz_libswift.ps1 index 8b7b7406..9c0b3c32 100644 --- a/dist/bundle_rz_libswift.ps1 +++ b/dist/bundle_rz_libswift.ps1 @@ -2,7 +2,7 @@ $dist = $args[0] $python = Split-Path((Get-Command python.exe).Path) if (-not (Test-Path -Path 'libswift' -PathType Container)) { - git clone https://github.com/rizinorg/rz-libswift.git --depth 1 --branch "v0.9.0" libswift + git clone https://github.com/rizinorg/rz-libswift.git --depth 1 libswift } cd libswift & meson.exe --buildtype=release --prefix=$dist build diff --git a/dist/bundle_rz_libyara.ps1 b/dist/bundle_rz_libyara.ps1 index 27711049..fe72a91b 100644 --- a/dist/bundle_rz_libyara.ps1 +++ b/dist/bundle_rz_libyara.ps1 @@ -3,7 +3,7 @@ $cmake_opts = $args[1] $python = Split-Path((Get-Command python.exe).Path) if (-not (Test-Path -Path 'rz_libyara' -PathType Container)) { - git clone https://github.com/rizinorg/rz-libyara.git --depth 1 --branch "v0.9.0" rz_libyara + git clone https://github.com/rizinorg/rz-libyara.git --depth 1 --branch main rz_libyara git -C rz_libyara submodule init git -C rz_libyara submodule update } diff --git a/dist/bundle_rz_silhouette.ps1 b/dist/bundle_rz_silhouette.ps1 index 77bbc9ef..4cf04668 100644 --- a/dist/bundle_rz_silhouette.ps1 +++ b/dist/bundle_rz_silhouette.ps1 @@ -3,7 +3,7 @@ $cmake_opts = $args[1] $python = Split-Path((Get-Command python.exe).Path) if (-not (Test-Path -Path 'rz-silhouette' -PathType Container)) { - git clone https://github.com/rizinorg/rz-silhouette.git --depth 1 --branch "v0.9.0-beta4" rz-silhouette + git clone https://github.com/rizinorg/rz-silhouette.git --depth 1 rz-silhouette } cd rz-silhouette & meson.exe --buildtype=release --prefix=$dist build diff --git a/rizin b/rizin index da228d11..136a337e 160000 --- a/rizin +++ b/rizin @@ -1 +1 @@ -Subproject commit da228d11cfc865b06442bd66b192caeb6e5ff556 +Subproject commit 136a337e1c40236723a58ccca6549aa577e8aaa5 diff --git a/scripts/fetch_deps.sh b/scripts/fetch_deps.sh index a6281d34..0753023f 100755 --- a/scripts/fetch_deps.sh +++ b/scripts/fetch_deps.sh @@ -5,7 +5,7 @@ set -e cd $(dirname "${BASH_SOURCE[0]}")/.. mkdir -p cutter-deps && cd cutter-deps -DEPS_BASE_URL=https://github.com/rizinorg/cutter-deps/releases/download/v18 +DEPS_BASE_URL=https://github.com/rizinorg/cutter-deps/releases/download/v19 if [ "$CUTTER_QT" == "5" ]; then DEPS_FILE_linux_x86_64=cutter-deps-q5-linux-x86_64.tar.gz @@ -13,18 +13,18 @@ if [ "$CUTTER_QT" == "5" ]; then DEPS_BASE_URL=https://github.com/rizinorg/cutter-deps/releases/download/qt5-v17 else DEPS_FILE_linux_x86_64=cutter-deps-linux-x86_64.tar.gz - DEPS_SHA256_linux_x86_64=204683c44e4824df7cb9925f1939544f081e64b7a701fe95fd03f67125a165b4 + DEPS_SHA256_linux_x86_64=b4bebf95c69f6216456d31b345d660ae3347517a6c14a4a4e9d568b03289c3de fi echo $DEPS_SHA256_linux_x86_64 DEPS_FILE_macos_x86_64=cutter-deps-macos-x86_64.tar.gz -DEPS_SHA256_macos_x86_64=a46c2dee12d4160410237da848075fc87c41c3850021f583d3b469a59c386650 +DEPS_SHA256_macos_x86_64=6f202bd2b18cae780d4fe4ddce662909eebb4f0cd3f1c685e5b2bb6cd46a3e7d DEPS_FILE_macos_arm64=cutter-deps-macos-arm64.tar.gz -DEPS_SHA256_macos_arm64=1ca381b562d632a370e05a85b0fe60a591a261bba0f48d97a0e40b4307c2577a +DEPS_SHA256_macos_arm64=fa4b3c58f02e7b57a18c2fe7b8c196dd008187deda72fa1ae685c8ed661e10c6 DEPS_FILE_win_x86_64=cutter-deps-win-x86_64.tar.gz -DEPS_SHA256_win_x86_64=95a3cf676178b8c58442d22ee8b21ce2530bfdae86f514901310490a8f28867a +DEPS_SHA256_win_x86_64=3d69280bfedfdd3363fac450f814fc2f09cdd6149f60541dcdaa81f782c3ac76 ARCH=x86_64 diff --git a/scripts/jsdec.sh b/scripts/jsdec.sh index 3ee28cb0..ebcd2c4c 100755 --- a/scripts/jsdec.sh +++ b/scripts/jsdec.sh @@ -9,7 +9,7 @@ SCRIPTPATH=$(realpath "$(dirname "${BASH_SOURCE[0]}")") cd "$SCRIPTPATH/.." if [ ! -d jsdec ]; then - git clone https://github.com/rizinorg/jsdec.git --depth 1 --branch "v0.9.0" + git clone https://github.com/rizinorg/jsdec.git --depth 1 --branch "dev" fi cd jsdec diff --git a/scripts/macos_sign.sh b/scripts/macos_sign.sh index 9d0b0c0c..c9f23a40 100755 --- a/scripts/macos_sign.sh +++ b/scripts/macos_sign.sh @@ -108,9 +108,17 @@ resign_dmg() { notarize_bundle Cutter-rw/Cutter.app unmount trap - EXIT + # Remove temporary signing space and restore HFS+ volume consistency. + ee hdiutil resize -size min Cutter-rw.dmg OUTPUT="${1%.*}-signed.dmg" echo_step "Creating final read-only ${OUTPUT}" ee hdiutil convert -format UDZO -o "${OUTPUT}" Cutter-rw.dmg + echo_step "Verifying filesystem in ${OUTPUT}" + ee hdiutil attach "${OUTPUT}" -readonly -nobrowse -noautofsck -mountpoint Cutter-rw + trap unmount EXIT + ee diskutil verifyVolume Cutter-rw + unmount + trap - EXIT } case "$1" in diff --git a/scripts/rz-libswift.sh b/scripts/rz-libswift.sh index c620ad74..0772cb4c 100755 --- a/scripts/rz-libswift.sh +++ b/scripts/rz-libswift.sh @@ -7,7 +7,7 @@ SCRIPTPATH=$(realpath "$(dirname "${BASH_SOURCE[0]}")") cd "$SCRIPTPATH/.." if [[ ! -d libswift ]]; then - git clone https://github.com/rizinorg/rz-libswift.git --depth 1 --branch "v0.9.0" libswift + git clone https://github.com/rizinorg/rz-libswift.git --depth 1 libswift fi cd libswift diff --git a/scripts/rz-libyara.sh b/scripts/rz-libyara.sh index 2c25ce25..06fdecd7 100755 --- a/scripts/rz-libyara.sh +++ b/scripts/rz-libyara.sh @@ -8,7 +8,7 @@ EXTRA_CMAKE_OPTS="$2" cd "$SCRIPTPATH/.." if [[ ! -d rz_libyara ]]; then - git clone https://github.com/rizinorg/rz-libyara.git --depth 1 --branch "v0.9.0" rz_libyara + git clone https://github.com/rizinorg/rz-libyara.git --depth 1 --branch main rz_libyara git -C rz_libyara submodule init git -C rz_libyara submodule update fi diff --git a/scripts/rz-silhouette.sh b/scripts/rz-silhouette.sh index 67430338..f2f595e5 100755 --- a/scripts/rz-silhouette.sh +++ b/scripts/rz-silhouette.sh @@ -8,7 +8,7 @@ EXTRA_CMAKE_OPTS="$2" cd "$SCRIPTPATH/.." if [[ ! -d rz-silhouette ]]; then - git clone https://github.com/rizinorg/rz-silhouette.git --depth 1 --branch "v0.9.0-beta4" rz-silhouette + git clone https://github.com/rizinorg/rz-silhouette.git --depth 1 rz-silhouette fi cd rz-silhouette diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 58b2b3b8..4fba081a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -613,7 +613,9 @@ if(CUTTER_ENABLE_PACKAGING) target_compile_definitions(Cutter PRIVATE CUTTER_ENABLE_PACKAGING) endif() -include(Translations) +if(CUTTER_ENABLE_TRANSLATIONS) + include(Translations) +endif() # Install files install(TARGETS Cutter diff --git a/src/common/Helpers.cpp b/src/common/Helpers.cpp index d50bd5b4..0f94847b 100644 --- a/src/common/Helpers.cpp +++ b/src/common/Helpers.cpp @@ -4,19 +4,55 @@ #include #include +#include #include #include #include #include +#include #include #include #include +#include #include #include #include #include +namespace { +template +int calculateMaxDisplayedLines(TextEditType *textEdit) +{ + qreal lineHeight = 0; + const QTextBlock firstBlock = textEdit->document()->begin(); + if (firstBlock.isValid()) { + lineHeight = textEdit->document()->documentLayout()->blockBoundingRect(firstBlock).height(); + } + + if (lineHeight <= 0) { + const QFontMetricsF fm(textEdit->font()); + lineHeight = fm.lineSpacing(); + } + + qreal availableHeight = textEdit->viewport()->height(); + const qreal margin = textEdit->document()->documentMargin(); + availableHeight -= margin; + + const qreal exactLines = availableHeight / lineHeight; + const qreal floorValue = std::floor(exactLines); + const qreal fractionalPart = exactLines - floorValue; + + // If the last line is more than 15% visible then count it, otherwise leave it + // avoids the issue where there is empty space at the bottom of a textedit widget + if (fractionalPart >= 0.15) { + return std::ceil(availableHeight / lineHeight); + } + + return floorValue; +} +} + namespace qhelpers { QString formatByteCount(ut64 bytecount) @@ -174,20 +210,12 @@ void SizePolicyMinMax::restoreHeight(QWidget *widget) const int getMaxFullyDisplayedLines(QTextEdit *textEdit) { - const QFontMetrics fontMetrics(textEdit->document()->defaultFont()); - return (textEdit->height() - - (textEdit->contentsMargins().top() + textEdit->contentsMargins().bottom() - + (int)(textEdit->document()->documentMargin() * 2))) - / fontMetrics.lineSpacing(); + return calculateMaxDisplayedLines(textEdit); } int getMaxFullyDisplayedLines(QPlainTextEdit *plainTextEdit) { - const QFontMetrics fontMetrics(plainTextEdit->document()->defaultFont()); - return (plainTextEdit->height() - - (plainTextEdit->contentsMargins().top() + plainTextEdit->contentsMargins().bottom() - + (int)(plainTextEdit->document()->documentMargin() * 2))) - / fontMetrics.lineSpacing(); + return calculateMaxDisplayedLines(plainTextEdit); } QByteArray applyColorToSvg(const QByteArray &data, QColor color) diff --git a/src/core/Cutter.cpp b/src/core/Cutter.cpp index 10178b86..425c3ac8 100644 --- a/src/core/Cutter.cpp +++ b/src/core/Cutter.cpp @@ -1187,19 +1187,15 @@ void CutterCore::setConfig(const char *k, const RzInterval &itv) void CutterCore::setConfig(const char *k, const QStringList &list) { CORE_LOCK(); - RzList *rzList = rz_list_newf(free); - if (!rzList) { + RzSetS *set = rz_set_s_new(HT_STR_DUP); + if (!set) { return; } - for (const QString &str : list) { - char *dupStr = strdup(str.toUtf8().constData()); - if (dupStr) { - rz_list_append(rzList, dupStr); - } + rz_set_s_add(set, str.toUtf8().constData()); } - - rz_config_set_list(core->config, k, rzList); + rz_config_set_set(core->config, k, set); + rz_set_s_free(set); } int CutterCore::getConfigi(const char *k) @@ -1226,16 +1222,12 @@ RzInterval CutterCore::getConfigItv(const char *k) return rz_config_get_interval(core->config, k); } -QStringList CutterCore::getConfigList(const char *k) +QSet CutterCore::getConfigSet(const char *k) { CORE_LOCK(); - - QStringList res; - RzList *list = rz_config_get_list(core->config, k); - for (const auto *s : CutterRzList(list)) { - res << QString::fromUtf8(s); - } - rz_list_free(list); + RzSetS *set = rz_config_get_set(core->config, k); + QSet res = convertRzSetS(set); + rz_set_s_free(set); return res; } @@ -1295,18 +1287,18 @@ QString CutterCore::getConfig(const char *k) return { rz_config_get(core->config, k) }; } -QStringList CutterCore::getConfigOptions(const char *k) +QSet CutterCore::getConfigOptions(const char *k) { CORE_LOCK(); const RzConfigNode *node = rz_config_node_get(core->config, k); if (!(node && node->options)) { return {}; } - QStringList list; - for (const auto &s : CutterRzList(node->options)) { - list << s; + QSet res; + for (auto it = CutterRzIter(rz_set_s_as_iter(node->options)); it; ++it) { + res << QString::fromUtf8(*it); } - return list; + return res; } void CutterCore::setConfig(const char *k, const QVariant &v) @@ -4154,13 +4146,11 @@ QList CutterCore::getAllEvaluableVars() var.type = EvaluableVarDescription::Interval; const RzInterval itv = rz_config_var_get_interval(v); value = QVariant::fromValue(itv); - } else if (RZ_CONFIG_VAR_IS_TYPE(flags, RZ_CONFIG_VAR_TYPE_LIST)) { - var.type = EvaluableVarDescription::List; - const RzList *list = rz_config_var_get_list(v); - QStringList stringList; - for (const auto *c : CutterRzList(list)) { - stringList << QString(c); - } + } else if (RZ_CONFIG_VAR_IS_TYPE(flags, RZ_CONFIG_VAR_TYPE_SET)) { + var.type = EvaluableVarDescription::Set; + RzSetS *set = rz_config_var_get_set(v); + value = QVariant::fromValue(convertRzSetS(set)); + rz_set_s_free(set); } if (value.isNull()) { @@ -4168,13 +4158,9 @@ QList CutterCore::getAllEvaluableVars() } var.value = value; - const RzList *optionsList = rz_config_var_get_options(v); - if (optionsList) { - RzListIter *iter; - char *option; - CutterRzListForeach (optionsList, iter, char, option) { - var.options << QString::fromUtf8(option); - } + const RzSetS *optionsSet = rz_config_var_get_options(v); + if (optionsSet) { + var.options = convertRzSetS(optionsSet); } } else { const RzConfigNode *node = &entry->node; @@ -4189,10 +4175,8 @@ QList CutterCore::getAllEvaluableVars() var.value = QString::fromUtf8(rz_config_entry_get_as_string(entry)); - RzListIter *iter; - char *option; - CutterRzListForeach (node->options, iter, char, option) { - var.options << QString::fromUtf8(option); + if (node->options) { + var.options = convertRzSetS(node->options); } } diff --git a/src/core/Cutter.h b/src/core/Cutter.h index f57f2eab..423c6032 100644 --- a/src/core/Cutter.h +++ b/src/core/Cutter.h @@ -495,12 +495,12 @@ public: bool getConfigb(const QString &k) { return getConfigb(k.toUtf8().constData()); } RzInterval getConfigItv(const char *k); RzInterval getConfigItv(const QString &k) { return getConfigItv(k.toUtf8().constData()); } - QStringList getConfigList(const char *k); - QStringList getConfigList(const QString &k) { return getConfigList(k.toUtf8().constData()); } + QSet getConfigSet(const char *k); + QSet getConfigSet(const QString &k) { return getConfigSet(k.toUtf8().constData()); } QString getConfig(const char *k); QString getConfig(const QString &k) { return getConfig(k.toUtf8().constData()); } QString getConfigDescription(const char *k); - QStringList getConfigOptions(const char *k); + QSet getConfigOptions(const char *k); QStringList getColorThemes(); QHash getTheme(); QStringList getThemeKeys(); diff --git a/src/core/CutterDescriptions.h b/src/core/CutterDescriptions.h index 0f23398b..0eea0278 100644 --- a/src/core/CutterDescriptions.h +++ b/src/core/CutterDescriptions.h @@ -10,8 +10,8 @@ #include #include #include +#include #include -#include #include struct FunctionDescription @@ -448,11 +448,11 @@ struct EvaluableVarDescription QString description; bool readOnly; - enum Type : ut8 { Bool = 0, Int, String, Interval, List }; + enum Type : ut8 { Bool = 0, Int, String, Interval, Set }; Type type; - QVariant value; ///< Can be either QString, QStringList or RzInterval depending on type - QList options; + QVariant value; ///< Can be either QString, QSet or RzInterval depending on type + QSet options; }; Q_DECLARE_METATYPE(FunctionDescription) diff --git a/src/core/RizinCpp.cpp b/src/core/RizinCpp.cpp index 594dfff6..559a7c8f 100644 --- a/src/core/RizinCpp.cpp +++ b/src/core/RizinCpp.cpp @@ -1 +1,12 @@ #include "RizinCpp.h" + +#include + +QSet convertRzSetS(const RzSetS *set) +{ + QSet res; + for (auto it = CutterRzIter(rz_set_s_as_iter(set)); it; ++it) { + res << QString::fromUtf8(*it); + } + return res; +} diff --git a/src/core/RizinCpp.h b/src/core/RizinCpp.h index b1a39dbd..407ba221 100644 --- a/src/core/RizinCpp.h +++ b/src/core/RizinCpp.h @@ -187,9 +187,9 @@ public: return *this; } operator bool() { return rzIter && rzIter->cur; } - T &operator*() { return *reinterpret_cast(rzIter->cur); } - T *get() { return reinterpret_cast(rzIter->cur); } - T *operator->() { return reinterpret_cast(rzIter->cur); } + T &operator*() { return *reinterpret_cast(rzIter->cur); } + T *get() { return reinterpret_cast(rzIter->cur); } + T *operator->() { return reinterpret_cast(rzIter->cur); } }; #define CutterHtDef(xx, XX, K, VB) \ @@ -219,4 +219,6 @@ public: CutterHtDef(sp, SP, const char *, void *); +QSet convertRzSetS(const RzSetS *set); + #endif // RIZINCPP_H diff --git a/src/dialogs/preferences/RizinConfigOptionsWidget.cpp b/src/dialogs/preferences/RizinConfigOptionsWidget.cpp index 0f625b55..38db0430 100644 --- a/src/dialogs/preferences/RizinConfigOptionsWidget.cpp +++ b/src/dialogs/preferences/RizinConfigOptionsWidget.cpp @@ -125,7 +125,7 @@ QVariant RizinConfigOptionsModel::data(const QModelIndex &index, int role) const const auto itv = evalVar.value.value(); return QString("%1+%2").arg(rzAddressString(itv.addr), rzSizeString(itv.size)); } - if (evalVar.type == EvaluableVarDescription::List) { + if (evalVar.type == EvaluableVarDescription::Set) { const QStringList list = evalVar.value.toStringList(); if (list.isEmpty()) { return QString(); @@ -344,14 +344,16 @@ QWidget *RizinConfigOptionsDelegate::createEditor(QWidget *parent, if (evalVar.type == EvaluableVarDescription::Bool || evalVar.type == EvaluableVarDescription::Interval - || evalVar.type == EvaluableVarDescription::List) { + || evalVar.type == EvaluableVarDescription::Set) { // handled in lambda connected to ui->treeView::doubleClicked return nullptr; } if (!evalVar.options.isEmpty()) { auto *comboBox = new QComboBox(parent); - for (const auto &opt : evalVar.options) { + QStringList opts = evalVar.options.values(); + opts.sort(); + for (const auto &opt : opts) { comboBox->addItem(opt); } return comboBox; @@ -486,7 +488,7 @@ RizinConfigOptionsWidget::RizinConfigOptionsWidget(PreferencesDialog *parent) return; } - if (evalVar.type == EvaluableVarDescription::List) { + if (evalVar.type == EvaluableVarDescription::Set) { StringListDialog dialog(evalVar.value.toStringList(), this); dialog.setWindowTitle(tr("Edit List for %1").arg(evalVar.name)); @@ -596,8 +598,8 @@ void RizinConfigOptionsWidget::handleConfigOptionChanged(const QModelIndex &topL if (!originalValues.contains(evalVar.name)) { if (evalVar.type == EvaluableVarDescription::Interval) { originalValues[evalVar.name] = QVariant::fromValue(Core()->getConfigItv(evalVar.name)); - } else if (evalVar.type == EvaluableVarDescription::List) { - originalValues[evalVar.name] = QVariant::fromValue(Core()->getConfigList(evalVar.name)); + } else if (evalVar.type == EvaluableVarDescription::Set) { + originalValues[evalVar.name] = QVariant::fromValue(Core()->getConfigSet(evalVar.name)); } else { originalValues[evalVar.name] = Core()->getConfig(evalVar.name); } diff --git a/src/shortcuts/DefaultShortcuts.cpp b/src/shortcuts/DefaultShortcuts.cpp index f93d7c2b..2ef4496b 100644 --- a/src/shortcuts/DefaultShortcuts.cpp +++ b/src/shortcuts/DefaultShortcuts.cpp @@ -215,6 +215,10 @@ const QHash &getDefaultShortcuts() { { Qt::Key_Space }, QT_TRANSLATE_NOOP("DisassemblyWidget", "Switch to Graph"), "DisassemblyWidget" } }, + { "Disassembly.moveLeft", + { QList { Qt::Key_H } + + QKeySequence::keyBindings(QKeySequence::MoveToPreviousChar), + QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Down"), "DisassemblyWidget" } }, { "Disassembly.moveDown", { QList { Qt::Key_J } + QKeySequence::keyBindings(QKeySequence::MoveToNextLine), @@ -223,6 +227,10 @@ const QHash &getDefaultShortcuts() { QList { Qt::Key_K } + QKeySequence::keyBindings(QKeySequence::MoveToPreviousLine), QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Up"), "DisassemblyWidget" } }, + { "Disassembly.moveRight", + { QList { Qt::Key_L } + + QKeySequence::keyBindings(QKeySequence::MoveToNextChar), + QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Up"), "DisassemblyWidget" } }, { "Disassembly.pageDown", { QKeySequence::keyBindings(QKeySequence::MoveToNextPage), QT_TRANSLATE_NOOP("DisassemblyWidget", "Move Cursor Down By Page"), diff --git a/src/widgets/DisassemblyWidget.cpp b/src/widgets/DisassemblyWidget.cpp index da0d49d1..87fbcb42 100644 --- a/src/widgets/DisassemblyWidget.cpp +++ b/src/widgets/DisassemblyWidget.cpp @@ -13,6 +13,7 @@ #include "widgets/AddressRangeScrollBar.h" #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include #include @@ -100,8 +102,6 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main) setupFonts(); setupColors(); - updateMaxLines(); - mDisasTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mDisasTextEdit->setFont(Config()->getFont()); mDisasTextEdit->setReadOnly(true); @@ -140,18 +140,27 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main) connect(Core(), &CutterCore::functionRenamed, this, refresh); connect(Core(), &CutterCore::varsChanged, this, refresh); connect(Core(), &CutterCore::asmOptionsChanged, this, refresh); - connect(Core(), &CutterCore::refreshCodeViews, this, refresh); + connect(Core(), &CutterCore::refreshCodeViews, this, [this] { + breakpointsDirty = true; + refreshDisasm(); + }); connect(Core(), &CutterCore::instructionChanged, this, &DisassemblyWidget::instructionChanged); - connect(Core(), &CutterCore::breakpointsChanged, this, &DisassemblyWidget::refreshIfInRange); + connect(Core(), &CutterCore::breakpointsChanged, this, [this](RVA offset) { + breakpointsDirty = true; + refreshIfInRange(offset); + }); connect(Config(), &Configuration::fontsUpdated, this, &DisassemblyWidget::fontsUpdatedSlot); connect(Config(), &Configuration::colorsUpdated, this, &DisassemblyWidget::colorsUpdatedSlot); - connect(Core(), &CutterCore::refreshAll, this, - [this]() { refreshDisasm(seekable->getOffset()); }); + connect(Core(), &CutterCore::refreshAll, this, [this]() { + // just in case if breakpoints were changed via rizin console + breakpointsDirty = true; + refreshDisasm(seekable->getOffset()); + }); refreshDisasm(seekable->getOffset()); - connect(mCtxMenu, &DisassemblyContextMenu::copy, mDisasTextEdit, &QPlainTextEdit::copy); + connect(mCtxMenu, &DisassemblyContextMenu::copy, this, &DisassemblyWidget::copySelection); mCtxMenu->addSeparator(); mCtxMenu->addAction(&syncAction); @@ -174,15 +183,16 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main) ADD_ACTION("General.seekPrev", Qt::WidgetWithChildrenShortcut, &DisassemblyWidget::seekPrev) - ADD_ACTION("Disassembly.moveDown", Qt::WidgetWithChildrenShortcut, - [this]() { moveCursorRelative(false, false); }) - ADD_ACTION("Disassembly.moveUp", Qt::WidgetWithChildrenShortcut, - [this]() { moveCursorRelative(true, false); }) - ADD_ACTION("Disassembly.pageDown", Qt::WidgetWithChildrenShortcut, - [this]() { moveCursorRelative(false, true); }) ADD_ACTION("Disassembly.pageUp", Qt::WidgetWithChildrenShortcut, - [this]() { moveCursorRelative(true, true); }) + [this] { moveCursorRelative(QTextCursor::Up, true); }); + + ADD_ACTION("Disassembly.pageDown", Qt::WidgetWithChildrenShortcut, + [this] { moveCursorRelative(QTextCursor::Down, true); }); + #undef ADD_ACTION + + QTimer::singleShot(0, [this] { updateMaxLines(); }); + connect(this, &CutterDockWidget::becameVisibleToUser, this, [this] { updateMaxLines(); }); } void DisassemblyWidget::setPreviewMode(bool previewMode) @@ -240,7 +250,7 @@ void DisassemblyWidget::instructionChanged(RVA offset) refreshDisasm(); } -void DisassemblyWidget::refreshDisasm(RVA offset) +void DisassemblyWidget::refreshDisasm(RVA offset, RefreshMode mode) { if (!disasmRefresh->attemptRefresh(offset == RVA_INVALID ? nullptr : new RVA(offset))) { return; @@ -261,30 +271,72 @@ void DisassemblyWidget::refreshDisasm(RVA offset) return; } - breakpoints = Core()->getBreakpointsAddresses(); + if (breakpointsDirty) { + breakpoints = Core()->getBreakpointsAddresses(); + breakpointsDirty = false; + } const int horizontalScrollValue = mDisasTextEdit->horizontalScrollBar()->value(); - mDisasTextEdit->setLockScroll(true); // avoid flicker + mDisasTextEdit->setLockScroll(true); // Retrieve disassembly lines { TempConfig tempConfig; tempConfig.set("scr.color", COLOR_MODE_16M).set("asm.lines", false); - lines = Core()->disassembleLines(topOffset, maxLines); + + if (mode == RefreshMode::Reset || lines.isEmpty()) { + lines = Core()->disassembleLines(topOffset, maxLines); + startIndex = 0; + } else if (mode == RefreshMode::Append) { + const RVA newOffset = Core()->nextOpAddr(lines.last().offset, 1); + lines.append(Core()->disassembleLines(newOffset, maxLines)); + + } else if (mode == RefreshMode::Prepend) { + const RVA newOffset = Core()->prevOpAddr(lines.first().offset, maxLines); + auto newLines = Core()->disassembleLines(newOffset, maxLines); + + // remove duplicates if any + if (!newLines.isEmpty() && !lines.isEmpty()) { + int keepSize = newLines.size(); + const RVA firstOffset = lines.first().offset; + for (int i = newLines.size() - 1; i >= 0; --i) { + if (newLines[i].offset >= firstOffset) { + keepSize = i; + } else { + break; + } + } + if (keepSize < newLines.size()) { + newLines.erase(newLines.begin() + keepSize, newLines.end()); + } + } + + const int prependCount = newLines.size(); + newLines.append(lines); + lines = std::move(newLines); + + startIndex += prependCount; + } } - connectCursorPositionChanged(true); + startIndex = qMax(0, qMin(startIndex, static_cast(lines.size()) - 1)); + endIndex = qMin(lines.size(), startIndex + maxLines); + // render lines + connectCursorPositionChanged(true); mDisasTextEdit->document()->clear(); QTextCursor cursor(mDisasTextEdit->document()); + cursor.beginEditBlock(); + const QTextBlockFormat regular = cursor.blockFormat(); - for (const DisassemblyLine &line : lines) { - if (line.offset < topOffset) { // overflow - break; - } + const QColor breakpointBg(ConfigColor("gui.breakpoint_background")); + + for (int i = startIndex; i < endIndex; ++i) { + const auto &line = lines[i]; + cursor.insertHtml(line.text); if (Core()->isBreakpoint(breakpoints, line.offset)) { QTextBlockFormat f; - f.setBackground(ConfigColor("gui.breakpoint_background")); + f.setBackground(breakpointBg); cursor.setBlockFormat(f); } auto a = new DisassemblyTextBlockUserData(line); @@ -293,8 +345,10 @@ void DisassemblyWidget::refreshDisasm(RVA offset) cursor.setBlockFormat(regular); } + cursor.endEditBlock(); + if (!lines.isEmpty()) { - bottomOffset = lines[qMin(lines.size(), maxLines) - 1].offset; + bottomOffset = lines[endIndex - 1].offset; if (bottomOffset < topOffset) { bottomOffset = RVA_MAX; } @@ -302,24 +356,54 @@ void DisassemblyWidget::refreshDisasm(RVA offset) bottomOffset = topOffset; } + // update cursor position + connectCursorPositionChanged(false); + updateCursorPosition(); + connectCursorPositionChanged(true); + + // update cursor selection + const bool hasSelection = (selectionAnchorRVA != RVA_INVALID && selectionPosRVA != RVA_INVALID + && (selectionAnchorRVA != selectionPosRVA + || selectionAnchorSubIndex != selectionPosSubIndex + || selectionAnchorChar != selectionPosChar)); + if (hasSelection) { + updateSelection(); + } connectCursorPositionChanged(false); - updateCursorPosition(); - - // remove additional lines - QTextCursor tc = mDisasTextEdit->textCursor(); - tc.movePosition(QTextCursor::Start); - tc.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, maxLines - 1); - tc.movePosition(QTextCursor::EndOfLine); - tc.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); - tc.removeSelectedText(); + // update ctx menu + updateContextMenuSelection(hasSelection); + // update scrollbars mDisasTextEdit->setLockScroll(false); mDisasTextEdit->horizontalScrollBar()->setValue(horizontalScrollValue); + mDisasTextEdit->verticalScrollBar()->setValue(0); + mDisasScrollArea->verticalScrollBar()->setPosition(topOffset); - // Refresh the left panel (trigger paintEvent) + // update left panel (trigger paint event) leftPanel->update(); + + // update buffer by erasing extra lines + constexpr int multiplier = 5; + const int targetSize = maxLines * multiplier; + if (lines.size() <= targetSize) { + return; + } + + // TODO: its fine for now but maybe in future this could be made more efficie + // if the complexity trade-off is bearable + const int idealKeepStart = startIndex - (targetSize - maxLines) / 2; + const int keepStart = + qMax(0, qMin(idealKeepStart, static_cast(lines.size() - targetSize))); + const int removeBottom = lines.size() - (keepStart + targetSize); + if (removeBottom > 0) { + lines.erase(lines.begin() + keepStart + targetSize, lines.begin() + lines.size()); + } + if (keepStart > 0) { + lines.erase(lines.begin(), lines.begin() + keepStart); + startIndex -= keepStart; + } } void DisassemblyWidget::scrollInstructions(int count, bool clampToScrollBarRange) @@ -328,16 +412,30 @@ void DisassemblyWidget::scrollInstructions(int count, bool clampToScrollBarRange return; } - RVA offset; - if (count > 0) { - offset = Core()->nextOpAddr(topOffset, count); - if (offset < topOffset) { - offset = RVA_MAX; - } + startIndex += count; + RefreshMode mode = RefreshMode::Reset; + if (startIndex < 0) { + mode = RefreshMode::Prepend; + } else if (startIndex + maxLines > lines.size()) { + mode = RefreshMode::Append; + } else if (startIndex >= 0 && startIndex < lines.size()) { + mode = RefreshMode::None; + } + + RVA offset = topOffset; + if (mode == RefreshMode::None) { + offset = lines[startIndex].offset; } else { - offset = Core()->prevOpAddr(topOffset, -count); - if (offset > topOffset) { - offset = 0; + if (count > 0) { + offset = Core()->nextOpAddr(topOffset, count); + if (offset < topOffset) { + offset = RVA_MAX; + } + } else { + offset = Core()->prevOpAddr(topOffset, -count); + if (offset > topOffset) { + offset = 0; + } } } @@ -345,10 +443,169 @@ void DisassemblyWidget::scrollInstructions(int count, bool clampToScrollBarRange offset = mDisasScrollArea->verticalScrollBar()->clampAddressToRange(offset); } - refreshDisasm(offset); + refreshDisasm(offset, mode); topOffsetHistory[topOffsetHistoryPos] = offset; } +void DisassemblyWidget::updateSelection() +{ + int anchorRenderIndex = -1; + int posRenderIndex = -1; + + auto findRenderIndex = [&](RVA offset, int offsetBlockIndex) -> int { + // safely large number to clamp the highlight off-screen + // without causing integer overflow during UI math + constexpr int offscreenPadding = 100000; + + if (lines.isEmpty()) { + return 0; + } + if (offset < lines.first().offset) { + return -offscreenPadding; + } + if (offset > lines.last().offset) { + return lines.size() + offscreenPadding; + } + + const int exactIndex = getLineIndex(offset, offsetBlockIndex); + if (exactIndex != -1) { + return exactIndex; + } + + // fallback: if somehow between bounds but missing, return nearest insertion point + auto it = std::lower_bound( + lines.begin(), lines.end(), offset, + [](const DisassemblyLine &line, RVA target) { return line.offset < target; }); + return std::distance(lines.begin(), it); + }; + + anchorRenderIndex = findRenderIndex(selectionAnchorRVA, selectionAnchorSubIndex); + posRenderIndex = findRenderIndex(selectionPosRVA, selectionPosSubIndex); + + // If the entire selection is completely above or completely below the screen then do nothing + if ((anchorRenderIndex < startIndex && posRenderIndex < startIndex) + || (anchorRenderIndex >= endIndex && posRenderIndex >= endIndex)) { + mDisasTextEdit->textCursor().clearSelection(); + return; + } + + // clamp indices so they don't reach outside current viewport + const int clampedAnchorIndex = qBound(startIndex, anchorRenderIndex, endIndex - 1); + const int clampedPosIndex = qBound(startIndex, posRenderIndex, endIndex - 1); + + const QTextBlock anchorBlock = + mDisasTextEdit->document()->findBlockByNumber(clampedAnchorIndex - startIndex); + const QTextBlock posBlock = + mDisasTextEdit->document()->findBlockByNumber(clampedPosIndex - startIndex); + + if (anchorBlock.isValid() && posBlock.isValid()) { + int finalAnchorPos = anchorBlock.position(); + int finalPosPos = posBlock.position(); + + // if selection starts above visible lines/blocks then start the selection from first + // character of first block otherwise start from where the selection actually started + if (anchorRenderIndex >= startIndex) { + finalAnchorPos += qMin(selectionAnchorChar, qMax(0, anchorBlock.length() - 1)); + } + + // same as above but for selection end + if (posRenderIndex < endIndex) { + finalPosPos += qMin(selectionPosChar, qMax(0, posBlock.length() - 1)); + } else { + // The position is below the screen - select till end of the last visible block + finalPosPos += qMax(0, posBlock.length() - 1); + } + + QTextCursor restoreCursor(mDisasTextEdit->document()); + restoreCursor.setPosition(finalAnchorPos); + restoreCursor.setPosition(finalPosPos, QTextCursor::KeepAnchor); + mDisasTextEdit->setTextCursor(restoreCursor); + } +} + +void DisassemblyWidget::updateContextMenuSelection(bool hasSelection) +{ + mCtxMenu->setCanCopy(hasSelection); + if (hasSelection) { + mCtxMenu->setCurHighlightedWord(mDisasTextEdit->textCursor().selectedText()); + } else { + mCtxMenu->setCurHighlightedWord(curHighlightedWord); + } +} + +int DisassemblyWidget::getLineIndex(RVA offset, int offsetSubIndex) const +{ + if (lines.isEmpty()) { + return -1; + } + + auto it = std::lower_bound( + lines.begin(), lines.end(), offset, + [](const DisassemblyLine &line, RVA target) { return line.offset < target; }); + + if (it != lines.end() && it->offset == offset) { + const int baseIndex = std::distance(lines.begin(), it); + if (baseIndex + offsetSubIndex < lines.size() + && lines[baseIndex + offsetSubIndex].offset == offset) { + return baseIndex + offsetSubIndex; + } + return baseIndex; + } + return -1; +} + +void DisassemblyWidget::updateLineHighlights() +{ + QList combinedSelections; + combinedSelections.append(highlightCurrentLine()); + combinedSelections.append(highlightPCLine()); + mDisasTextEdit->setExtraSelections(combinedSelections); +} + +void DisassemblyWidget::invalidateCursorSelection() +{ + selectionAnchorRVA = RVA_INVALID; + selectionPosRVA = RVA_INVALID; + selectionAnchorSubIndex = 0; + selectionPosSubIndex = 0; + selectionAnchorChar = 0; + selectionPosChar = 0; +} + +int DisassemblyWidget::getIndexInOffsetGroup(const QTextCursor &cursor) const +{ + const int lineIndex = startIndex + cursor.blockNumber(); + if (lineIndex < 0 || lineIndex >= lines.size()) { + return 0; + } + + const RVA offset = lines[lineIndex].offset; + int subIndex = 0; + + for (int i = lineIndex - 1; i >= 0; --i) { + if (lines[i].offset == offset) { + subIndex++; + } else { + break; // offsets are contiguous + } + } + return subIndex; +} + +void DisassemblyWidget::updateSelectionPos(const QTextCursor &cursor) +{ + selectionPosRVA = DisHlp::readDisassemblyOffset(cursor); + selectionPosSubIndex = getIndexInOffsetGroup(cursor); + selectionPosChar = cursor.positionInBlock(); +} + +void DisassemblyWidget::updateSelectionAnchor(const QTextCursor &cursor) +{ + selectionAnchorRVA = DisHlp::readDisassemblyOffset(cursor); + selectionAnchorSubIndex = getIndexInOffsetGroup(cursor); + selectionAnchorChar = cursor.positionInBlock(); +} + bool DisassemblyWidget::updateMaxLines() { const int currentMaxLines = qhelpers::getMaxFullyDisplayedLines(mDisasTextEdit); @@ -362,42 +619,61 @@ bool DisassemblyWidget::updateMaxLines() return false; } -void DisassemblyWidget::highlightCurrentLine() +QList DisassemblyWidget::highlightCurrentLine() { QList extraSelections; const QColor highlightColor = ConfigColor("lineHighlight"); - // Highlight the current word + const RVA currentOffset = seekable->getOffset(); + + bool isOffScreen = true; + if (currentOffset >= topOffset + && (currentOffset <= bottomOffset || bottomOffset == RVA_INVALID)) { + const int targetLineIndex = getLineIndex(currentOffset, cursorLineOffset); + if (targetLineIndex >= startIndex && targetLineIndex < endIndex) { + isOffScreen = false; + } + } + QTextCursor cursor = mDisasTextEdit->textCursor(); - auto clickedCharPos = cursor.positionInBlock(); - // Select the line (BlockUnderCursor matches a line with current implementation) - cursor.select(QTextCursor::BlockUnderCursor); - // Remove any non-breakable space from the current line - const QString searchString = cursor.selectedText().replace("\xc2\xa0", " "); - // Cut the line in "tokens" that can be highlighted - static const QRegularExpression tokenRegExp(R"(\b(? clickedCharPos) { - curHighlightedWord = match.captured(); - break; + + if (!isOffScreen) { + if (cursor.hasSelection()) { + curHighlightedWord = cursor.selectedText(); + } else { + auto clickedCharPos = cursor.positionInBlock(); + // Select the line (BlockUnderCursor matches a line with current implementation) + cursor.select(QTextCursor::BlockUnderCursor); + // Remove any non-breakable space from the current line + const QString searchString = cursor.selectedText().replace("\xc2\xa0", " "); + // Cut the line in "tokens" that can be highlighted + static const QRegularExpression tokenRegExp(R"(\b(? clickedCharPos) { + curHighlightedWord = match.captured(); + break; + } + } } } // Highlight the current line QTextEdit::ExtraSelection highlightSelection; - highlightSelection.cursor = cursor; + highlightSelection.cursor = mDisasTextEdit->textCursor(); highlightSelection.cursor.movePosition(QTextCursor::Start); while (true) { const RVA lineOffset = DisHlp::readDisassemblyOffset(highlightSelection.cursor); - if (lineOffset == seekable->getOffset()) { + if (lineOffset == currentOffset) { highlightSelection.format.setBackground(highlightColor); highlightSelection.format.setProperty(QTextFormat::FullWidthSelection, true); highlightSelection.cursor.clearSelection(); extraSelections.append(highlightSelection); - } else if (lineOffset != RVA_INVALID && lineOffset > seekable->getOffset()) { + } else if (lineOffset != RVA_INVALID && lineOffset > currentOffset) { break; } highlightSelection.cursor.movePosition(QTextCursor::EndOfLine); @@ -409,12 +685,14 @@ void DisassemblyWidget::highlightCurrentLine() } // Highlight all the words in the document same as the current one - extraSelections.append(createSameWordsSelections(mDisasTextEdit, curHighlightedWord)); + if (!curHighlightedWord.isEmpty()) { + extraSelections.append(createSameWordsSelections(mDisasTextEdit, curHighlightedWord)); + } - mDisasTextEdit->setExtraSelections(extraSelections); + return extraSelections; } -void DisassemblyWidget::highlightPCLine() +QList DisassemblyWidget::highlightPCLine() { const RVA pcAddr = Core()->getProgramCounterValue(); @@ -444,11 +722,7 @@ void DisassemblyWidget::highlightPCLine() } } - // Don't override any extraSelections already set - QList currentSelections = mDisasTextEdit->extraSelections(); - currentSelections.append(pcSelections); - - mDisasTextEdit->setExtraSelections(currentSelections); + return pcSelections; } void DisassemblyWidget::showDisasContextMenu(const QPoint &pt) @@ -466,63 +740,37 @@ void DisassemblyWidget::updateCursorPosition() { const RVA offset = seekable->getOffset(); - // already fine where it is? - const RVA currentLineOffset = readCurrentDisassemblyOffset(); - if (currentLineOffset == offset) { - return; - } - connectCursorPositionChanged(true); + const int targetLineIndex = getLineIndex(offset, cursorLineOffset); + + bool isOffScreen = false; if (offset < topOffset || (offset > bottomOffset && bottomOffset != RVA_INVALID)) { - mDisasTextEdit->moveCursor(QTextCursor::Start); - mDisasTextEdit->setExtraSelections( - createSameWordsSelections(mDisasTextEdit, curHighlightedWord)); - } else { - const RVA currentCursorOffset = readCurrentDisassemblyOffset(); - const QTextCursor originalCursor = mDisasTextEdit->textCursor(); - - QTextCursor cursor = originalCursor; - cursor.movePosition(QTextCursor::Start); - - while (true) { - const RVA lineOffset = DisHlp::readDisassemblyOffset(cursor); - if (lineOffset == offset) { - if (cursorLineOffset > 0) { - cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, - cursorLineOffset); - } - if (cursorCharOffset > 0) { - cursor.movePosition(QTextCursor::StartOfLine); - cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, - cursorCharOffset); - } - - mDisasTextEdit->setTextCursor(cursor); - highlightCurrentLine(); - break; - } else if (lineOffset != RVA_INVALID && lineOffset > offset) { - mDisasTextEdit->moveCursor(QTextCursor::Start); - mDisasTextEdit->setExtraSelections({}); - break; - } - - cursor.movePosition(QTextCursor::EndOfLine); - if (cursor.atEnd()) { - break; - } - - cursor.movePosition(QTextCursor::Down); - } - - // this is true if a seek came from the user clicking on a line. - // then the cursor should be restored 1:1 to retain selection and cursor position. - if (currentCursorOffset == offset) { - mDisasTextEdit->setTextCursor(originalCursor); - } + isOffScreen = true; + } else if (targetLineIndex < startIndex || targetLineIndex >= endIndex) { + isOffScreen = true; } - highlightPCLine(); + if (isOffScreen) { + mDisasTextEdit->moveCursor(QTextCursor::Start); + mDisasTextEdit->setCursorVisible(false); + } else { + const int targetBlockNum = targetLineIndex - startIndex; + QTextCursor cursor = mDisasTextEdit->textCursor(); + cursor.movePosition(QTextCursor::Start); + if (targetBlockNum > 0) { + cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, targetBlockNum); + } + if (cursorCharOffset > 0) { + cursor.movePosition(QTextCursor::StartOfLine); + cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, cursorCharOffset); + } + + mDisasTextEdit->setTextCursor(cursor); + mDisasTextEdit->setCursorVisible(true); + } + + updateLineHighlights(); connectCursorPositionChanged(false); } @@ -542,35 +790,137 @@ void DisassemblyWidget::cursorPositionChanged() { const RVA offset = readCurrentDisassemblyOffset(); - cursorLineOffset = 0; - QTextCursor c = mDisasTextEdit->textCursor(); + const auto c = mDisasTextEdit->textCursor(); cursorCharOffset = c.positionInBlock(); - while (c.blockNumber() > 0) { - c.movePosition(QTextCursor::PreviousBlock); - if (DisHlp::readDisassemblyOffset(c) != offset) { - break; - } - cursorLineOffset++; - } + cursorLineOffset = getIndexInOffsetGroup(c); seekFromCursor = true; seekable->seek(offset); seekFromCursor = false; - highlightCurrentLine(); - highlightPCLine(); - mCtxMenu->setCanCopy(mDisasTextEdit->textCursor().hasSelection()); - if (mDisasTextEdit->textCursor().hasSelection()) { - // A word is selected so use it - mCtxMenu->setCurHighlightedWord(mDisasTextEdit->textCursor().selectedText()); - } else { - // No word is selected so use the word under the cursor - mCtxMenu->setCurHighlightedWord(curHighlightedWord); - } + + updateLineHighlights(); + updateContextMenuSelection(mDisasTextEdit->textCursor().hasSelection()); leftPanel->update(); } -void DisassemblyWidget::moveCursorRelative(bool up, bool page) +void DisassemblyWidget::copySelection() { + if (selectionAnchorRVA == RVA_INVALID || selectionPosRVA == RVA_INVALID) { + mDisasTextEdit->copy(); + return; + } + + // determine direction (user could have dragged upwards) + bool forward = true; + if (selectionAnchorRVA > selectionPosRVA) { + forward = false; + } else if (selectionAnchorRVA == selectionPosRVA) { + forward = (selectionAnchorChar <= selectionPosChar); + } + + const RVA startRVA = forward ? selectionAnchorRVA : selectionPosRVA; + const RVA endRVA = forward ? selectionPosRVA : selectionAnchorRVA; + const int startChar = forward ? selectionAnchorChar : selectionPosChar; + const int endChar = forward ? selectionPosChar : selectionAnchorChar; + const int startRVAIndex = forward ? selectionAnchorSubIndex : selectionPosSubIndex; + const int endRVAIndex = forward ? selectionPosSubIndex : selectionAnchorSubIndex; + + QStringList plainTextLines; + QTextDocument htmlStripper; + + // check if the entire selection is currently sitting in 'lines' buffer + int selectionStart = -1, selectionEnd = -1; + if (!lines.isEmpty() && startRVA >= lines.first().offset && endRVA <= lines.last().offset) { + selectionStart = getLineIndex(startRVA, startRVAIndex); + selectionEnd = getLineIndex(endRVA, endRVAIndex); + } + + // extract text + if (selectionStart != -1 && selectionEnd != -1 && selectionStart <= selectionEnd) { + for (int i = selectionStart; i <= selectionEnd; ++i) { + htmlStripper.setHtml(lines[i].text); + plainTextLines.append(htmlStripper.toPlainText()); + } + } else { + // selection exceeds buffer + TempConfig tempConfig; + tempConfig.set("scr.color", COLOR_MODE_DISABLED).set("asm.lines", false); + + RVA currentRVA = startRVA; + int currentStartHit = 0; + int currentEndHit = 0; + + while (currentRVA <= endRVA && currentRVA != RVA_INVALID) { + const auto fetchedLines = Core()->disassembleLines(currentRVA, maxLines); + if (fetchedLines.isEmpty()) { + break; + } + + for (const auto &line : fetchedLines) { + // skip unselected duplicate lines at the top + if (line.offset == startRVA && currentStartHit < startRVAIndex) { + currentStartHit++; + continue; + } + + if (line.offset > endRVA) { + currentRVA = RVA_INVALID; // reached target + break; + } + + plainTextLines.append(QString(line.text).replace(" ", " ")); + + if (line.offset == endRVA) { + if (currentEndHit == endRVAIndex) { + currentRVA = RVA_INVALID; // reached target + break; + } + currentEndHit++; + } + } + + if (currentRVA != RVA_INVALID) { + currentRVA = Core()->nextOpAddr(fetchedLines.last().offset, 1); + } + } + } + + if (plainTextLines.isEmpty()) { + return; + } + + if (plainTextLines.size() == 1) { + // single-line selection + const int len = qMax(0, endChar - startChar); + plainTextLines[0] = plainTextLines[0].mid(startChar, len); + } else { + // multi-line selection + if (startChar > 0) { + plainTextLines[0] = plainTextLines[0].mid(startChar); + } + if (endChar >= 0 && endChar < plainTextLines.last().length()) { + plainTextLines.last() = plainTextLines.last().left(endChar); + } + } + + const QString finalCopiedText = plainTextLines.join("\n"); + QApplication::clipboard()->setText(finalCopiedText); +} + +void DisassemblyWidget::moveCursorRelative(QTextCursor::MoveOperation op, bool page) +{ + const bool up = op == QTextCursor::Up; + const bool select = QApplication::keyboardModifiers() & Qt::ShiftModifier; + const bool hadSelection = (selectionAnchorRVA != RVA_INVALID && selectionPosRVA != RVA_INVALID); + + if (select) { + if (!hadSelection) { + updateSelectionAnchor(mDisasTextEdit->textCursor()); + } + } else { + invalidateCursorSelection(); + } + if (page) { RVA offset; if (!up) { @@ -605,28 +955,53 @@ void DisassemblyWidget::moveCursorRelative(bool up, bool page) } } refreshDisasm(offset); - } else { // normal arrow keys - const int blockCount = mDisasTextEdit->blockCount(); - if (blockCount < 1) { + + if (select) { + const auto newCursor = mDisasTextEdit->textCursor(); + selectionPosRVA = DisHlp::readDisassemblyOffset(newCursor); + selectionPosSubIndex = getIndexInOffsetGroup(newCursor); + selectionPosChar = newCursor.positionInBlock(); + refreshDisasm(RVA_INVALID, RefreshMode::None); + } else if (hadSelection) { + refreshDisasm(RVA_INVALID, RefreshMode::None); + } + } else { + if (op == QTextCursor::Left || op == QTextCursor::Right) { + mDisasTextEdit->moveCursor(op, + select ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + + if (select) { + updateSelectionPos(mDisasTextEdit->textCursor()); + refreshDisasm(RVA_INVALID, RefreshMode::None); + } else if (hadSelection) { + refreshDisasm(RVA_INVALID, RefreshMode::None); + } return; } const int blockNumber = mDisasTextEdit->textCursor().blockNumber(); + const int count = endIndex - startIndex; - if (blockNumber == blockCount - 1 && !up) { - scrollInstructions(1); - } else if (blockNumber == 0 && up) { + if (up && blockNumber <= 0) { scrollInstructions(-1); + } else if (op == QTextCursor::Down && blockNumber >= count - 1) { + scrollInstructions(1); } - mDisasTextEdit->moveCursor(up ? QTextCursor::Up : QTextCursor::Down); + mDisasTextEdit->moveCursor(op, select ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor); + mDisasTextEdit->verticalScrollBar()->setValue(startIndex - startIndex); + + if (select) { + updateSelectionPos(mDisasTextEdit->textCursor()); + refreshDisasm(RVA_INVALID, RefreshMode::None); + } - // handle cases where top instruction offsets change const RVA offset = readCurrentDisassemblyOffset(); if (offset != seekable->getOffset()) { seekable->seek(offset); - highlightCurrentLine(); - highlightPCLine(); + updateLineHighlights(); + } else if (!select && hadSelection) { + refreshDisasm(RVA_INVALID, RefreshMode::None); } } } @@ -693,6 +1068,54 @@ void DisassemblyWidget::keyPressEvent(QKeyEvent *event) } else { jumpToOffsetUnderCursor(cursor); } + event->accept(); + return; + } + + // handle cursor shortcuts here because we need to check if shift is pressed for + // updating selection. + // shift select is not supported for page up and page down currently + bool handled = false; + QTextCursor::MoveOperation op; + + // fix for macos, since macos appends KeypadModifier to cursor keys pressed via keypad, it + // causes the QKeySequence::ExactMatch check to fail. + // We remove the modifier here + const Qt::KeyboardModifiers pureModifiers = event->modifiers() & ~Qt::KeypadModifier; + + const int baseKey = event->key() | (pureModifiers & ~Qt::ShiftModifier); + QKeySequence baseSeq(baseKey); + QKeySequence exactSeq(event->key() | event->modifiers()); + + auto matches = [&](const QString &id) { + for (const QKeySequence &seq : Shortcuts()->getKeySequences(id)) { + if (seq.matches(baseSeq) == QKeySequence::ExactMatch + || seq.matches(exactSeq) == QKeySequence::ExactMatch) { + return true; + } + } + return false; + }; + + if (matches("Disassembly.moveUp")) { + op = QTextCursor::Up; + handled = true; + } else if (matches("Disassembly.moveDown")) { + op = QTextCursor::Down; + handled = true; + } else if (matches("Disassembly.moveLeft")) { + op = QTextCursor::Left; + handled = true; + } else if (matches("Disassembly.moveRight")) { + op = QTextCursor::Right; + handled = true; + } + + if (handled) { + moveCursorRelative(op, false); + mDisasTextEdit->setCursorVisible(true); + event->accept(); + return; } MemoryDockWidget::keyPressEvent(event); @@ -714,6 +1137,16 @@ QString DisassemblyWidget::getWindowTitle() const return tr("Disassembly"); } +int DisassemblyWidget::getEndIndex() const +{ + return endIndex; +} + +int DisassemblyWidget::getStartIndex() const +{ + return startIndex; +} + void DisassemblyWidget::onSeekChanged(RVA offset, CutterCore::SeekHistoryType type) { if (type == CutterCore::SeekHistoryType::New) { @@ -732,6 +1165,10 @@ void DisassemblyWidget::onSeekChanged(RVA offset, CutterCore::SeekHistoryType ty if (!seekFromCursor) { cursorLineOffset = 0; cursorCharOffset = 0; + + // invalidate selection if the seek came from an external + // source (like seeking via VisualNavBar) + invalidateCursorSelection(); } if (topOffset != RVA_INVALID && offset >= topOffset && offset <= bottomOffset @@ -831,11 +1268,35 @@ void DisassemblyScrollArea::wheelEvent(QWheelEvent *event) emit wheelEventTriggered(); } +DisassemblyTextEdit::DisassemblyTextEdit(DisassemblyWidget *disasmWidget) + : QPlainTextEdit(disasmWidget), lockScroll(false), disasmWidget(disasmWidget) +{ + + blinkTimer = new QTimer(this); + connect(blinkTimer, &QTimer::timeout, this, [this]() { + cursorVisible = !cursorVisible; + viewport()->update(); + }); + blinkTimer->start(500); +} + qreal DisassemblyTextEdit::textOffset() const { return (blockBoundingGeometry(document()->begin()).topLeft() + contentOffset()).y(); } +void DisassemblyTextEdit::setCursorVisible(bool visible) +{ + if (visible) { + cursorVisible = true; + blinkTimer->start(500); + } else { + cursorVisible = false; + blinkTimer->stop(); + } + viewport()->update(); +} + bool DisassemblyTextEdit::viewportEvent(QEvent *event) { switch (event->type()) { @@ -861,6 +1322,16 @@ void DisassemblyTextEdit::keyPressEvent(QKeyEvent *event) void DisassemblyTextEdit::mousePressEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton) { + + const QTextCursor cursor = cursorForPosition(event->pos()); + + disasmWidget->updateSelectionAnchor(cursor); + disasmWidget->updateSelectionPos(cursor); + + disasmWidget->refreshDisasm(RVA_INVALID, RefreshMode::None); + } + QPlainTextEdit::mousePressEvent(event); if (event->button() == Qt::RightButton && !textCursor().hasSelection()) { @@ -868,6 +1339,45 @@ void DisassemblyTextEdit::mousePressEvent(QMouseEvent *event) } } +void DisassemblyTextEdit::mouseMoveEvent(QMouseEvent *event) +{ + if (event->buttons() & Qt::LeftButton) { + + const QPoint pos = event->pos(); + const QTextCursor cursor = cursorForPosition(pos); + const RVA currentRVA = DisHlp::readDisassemblyOffset(cursor); + + if (currentRVA != RVA_INVALID) { + disasmWidget->updateSelectionPos(cursor); + } + + if (pos.y() < 0) { + disasmWidget->scrollInstructions(-1, true); + } else if (pos.y() > viewport()->height()) { + disasmWidget->scrollInstructions(1, true); + } else if (currentRVA != RVA_INVALID) { + disasmWidget->refreshDisasm(RVA_INVALID, RefreshMode::None); + } + + return; + } + + QPlainTextEdit::mouseMoveEvent(event); +} + +void DisassemblyTextEdit::paintEvent(QPaintEvent *event) +{ + QPlainTextEdit::paintEvent(event); + + if (cursorVisible) { + QPainter painter(viewport()); + QRect cRect = cursorRect(); + cRect.setWidth(2); + painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); + painter.fillRect(cRect, Qt::white); + } +} + void DisassemblyWidget::seekPrev() { Core()->seekPrev(); @@ -925,15 +1435,27 @@ void DisassemblyLeftPanel::paintEvent(QPaintEvent *event) cursor.movePosition(QTextCursor::Down); } + const int startIndex = disas->getStartIndex(); + const int endIndex = qMin(static_cast(lines.size()), disas->getEndIndex()); + const int maxLines = endIndex - startIndex; + + // Capture how many lines are ACTUALLY drawn on the screen right now + const int visibleCount = qMin(maxLines, static_cast(lines.size()) - startIndex); + if (visibleCount <= 0) { + return; + } + using LineInfo = std::pair; std::vector lineOffsets; - lineOffsets.reserve(lines.size() + arrows.size()); + lineOffsets.reserve(visibleCount + arrows.size()); - RVA minViewOffset = 0, maxViewOffset = 0; - minViewOffset = maxViewOffset = lines[0].offset; + RVA minViewOffset = lines[startIndex].offset; + RVA maxViewOffset = minViewOffset; - for (int i = 0; i < lines.size(); i++) { - lineOffsets.emplace_back(lines[i].offset, i); + for (int i = startIndex; i < endIndex; i++) { + // Map offset to a viewport-relative index (0 to maxLines) instead of absolute + // buffer index 'i' + lineOffsets.emplace_back(lines[i].offset, i - startIndex); minViewOffset = std::min(minViewOffset, lines[i].offset); maxViewOffset = std::max(maxViewOffset, lines[i].offset); if (lines[i].arrow != RVA_INVALID) { @@ -977,7 +1499,7 @@ void DisassemblyLeftPanel::paintEvent(QPaintEvent *event) [&](const LineInfo &line) { return line.first > maxViewOffset; }) - lineOffsets.begin(); for (size_t i = firstLineAfter; i < lineOffsets.size(); i++) { - lineOffsets[i].second = lines.size() + (i - firstLineAfter) + lineOffsets[i].second = visibleCount + (i - firstLineAfter) + 1; // +1 to ensure end of arrrow is drawn outside screen } @@ -997,12 +1519,11 @@ void DisassemblyLeftPanel::paintEvent(QPaintEvent *event) return res->second; }; - auto fitsInScreen = [&](const Arrow &a) { return maxViewOffset - minViewOffset < a.length(); }; - - std::sort(std::begin(arrows), std::end(arrows), [&](const Arrow &l, const Arrow &r) { - const int lScreen = fitsInScreen(l), rScreen = fitsInScreen(r); - if (lScreen != rScreen) { - return lScreen < rScreen; + std::sort(std::begin(arrows), std::end(arrows), [](const Arrow &l, const Arrow &r) { + const auto lLen = l.length(); + const auto rLen = r.length(); + if (lLen != rLen) { + return lLen < rLen; } return l.max != r.max ? l.max < r.max : l.min > r.min; }); @@ -1037,7 +1558,11 @@ void DisassemblyLeftPanel::paintEvent(QPaintEvent *event) const RVA currOffset = disas->getSeekable()->getOffset(); const qreal pixelRatio = qhelpers::devicePixelRatio(p.device()); - const Arrow visibleRange { lines.first().offset, lines.last().offset }; + + const int targetEndIndex = + qMin(static_cast(lines.size() - 1), startIndex + visibleCount - 1); + const Arrow visibleRange { lines.at(startIndex).offset, lines.at(targetEndIndex).offset }; + // Draw the lines for (const auto &arrow : arrows) { if (!visibleRange.intersects(arrow)) { diff --git a/src/widgets/DisassemblyWidget.h b/src/widgets/DisassemblyWidget.h index abd6d99b..a83b261f 100644 --- a/src/widgets/DisassemblyWidget.h +++ b/src/widgets/DisassemblyWidget.h @@ -19,6 +19,8 @@ class DisassemblyContextMenu; class DisassemblyLeftPanel; class AddressRangeScrollBar; +enum class RefreshMode : ut8 { Append, Prepend, Reset, Keep, None }; + /** * @brief Main widget for showing disassembly of a binary * @@ -33,48 +35,67 @@ public: static QString getWidgetType(); + QFontMetricsF getFontMetrics(); + QList getLines(); + + int getStartIndex() const; + int getEndIndex() const; + + /** + * @brief Updates the offset and character position where the cursor selection ends + */ + void updateSelectionPos(const QTextCursor &cursor); + + /** + * @brief Updates the offset and character position where the cursor selection starts + */ + void updateSelectionAnchor(const QTextCursor &cursor); + public slots: /** * @brief Highlights the currently selected line and updates the * highlighting of the same words under the cursor in the visible screen. - * This overrides all previous highlighting. + * @return List of selections to be highlighted */ - void highlightCurrentLine(); + QList highlightCurrentLine(); /** * @brief Adds the PC line highlighting to the other current highlighting. - * This should be called after highlightCurrentLine since that function - * overrides all previous highlighting. + * This is generally called after highlightCurrentLine + * @return List of selections to be highlighted */ - void highlightPCLine(); + QList highlightPCLine(); void showDisasContextMenu(const QPoint &pt); void fontsUpdatedSlot(); void colorsUpdatedSlot(); void scrollInstructions(int count, bool clampToScrollBarRange = false); void seekPrev(); void setPreviewMode(bool previewMode); - QFontMetricsF getFontMetrics(); - QList getLines(); /** * @brief Forces the transient vertical scrollbar to appear on scroll */ void showTransientScrollBar(); + + void refreshDisasm(RVA offset = RVA_INVALID, RefreshMode mode = RefreshMode::Reset); + protected slots: void onSeekChanged(RVA offset, CutterCore::SeekHistoryType type); void refreshIfInRange(RVA offset); void instructionChanged(RVA offset); - void refreshDisasm(RVA offset = RVA_INVALID); bool updateMaxLines(); void cursorPositionChanged(); + /** + * @brief Copies the currently highlighted disassembly text to the system clipboard + */ + void copySelection(); protected: DisassemblyContextMenu *mCtxMenu; DisassemblyScrollArea *mDisasScrollArea; DisassemblyTextEdit *mDisasTextEdit; DisassemblyLeftPanel *leftPanel; - QList lines; private: RVA topOffset; @@ -101,7 +122,31 @@ private: int topOffsetHistoryPos = 0; QList topOffsetHistory; + int startIndex = 0; + int endIndex = 0; + QList lines; + + // Cursor selection related + RVA selectionAnchorRVA = RVA_INVALID; + /** + * @brief metadata lines attached to instruction have the same offset as the instruction itself, + * this keeps track of which metdata line the cursor was at inside the offset block + */ + int selectionAnchorSubIndex = 0; + int selectionAnchorChar = 0; + RVA selectionPosRVA = RVA_INVALID; + /** + * @brief same use-case as @ref selectionAnchorSubIndex but for cursor position instead if + * anchor + */ + int selectionPosSubIndex = 0; + int selectionPosChar = 0; + QList breakpoints; + /** + * @brief Set whenever breakpoints have been updated or the screen has been manually refreshed + */ + bool breakpointsDirty = true; void setupFonts(); void setupColors(); @@ -110,9 +155,53 @@ private: void connectCursorPositionChanged(bool disconnect); - void moveCursorRelative(bool up, bool page); + void moveCursorRelative(QTextCursor::MoveOperation op, bool page); void jumpToOffsetUnderCursor(const QTextCursor &); + + /** + * @brief Visually highlights the text on screen between the selection start (anchor) and the + * current cursor position + */ + void updateSelection(); + void updateContextMenuSelection(bool hasSelection); + + /** + * @brief Finds the visual line number in the current view for a specific offset + * @param offset The memory address (RVA) to look for + * @param offsetSubIndex The specific sub-line to target if the instructions has metadata + * attached to it + * @return The index of the line, or -1 if not found + * + * @see DisassemblyHelper::getIndexInOffsetGroup() + */ + int getLineIndex(RVA offset, int offsetSubIndex) const; + /** + * @brief Refreshes the background colors in the disassembly view. + * It applies highlights to both the line currently under the user's cursor and the Program + * Counter (PC) line. + */ + void updateLineHighlights(); + /** + * @brief Clears the current text selection so nothing is highlighted as selected + */ + void invalidateCursorSelection(); + + /** + * @brief Metadata lines attached to an instruction have the same offset saved with it as the + * instruction, This function returns the index of the current line within that offset + * group/block + * * e.g: + * * @code + * ; a comment + * ; another comment <------ assume cursor is here + * ; void func1() + * 0x1000 mov rax, rax + * @endcode + * + * Then the returned index is "1" + */ + int getIndexInOffsetGroup(const QTextCursor &cursor) const; }; class DisassemblyScrollArea : public QAbstractScrollArea @@ -142,23 +231,30 @@ class DisassemblyTextEdit : public QPlainTextEdit Q_OBJECT public: - explicit DisassemblyTextEdit(QWidget *parent = nullptr) - : QPlainTextEdit(parent), lockScroll(false) - { - } + explicit DisassemblyTextEdit(DisassemblyWidget *disasmWidget = nullptr); void setLockScroll(bool lock) { this->lockScroll = lock; } qreal textOffset() const; + void setCursorVisible(bool visible); + protected: bool viewportEvent(QEvent *event) override; void scrollContentsBy(int dx, int dy) override; void keyPressEvent(QKeyEvent *event) override; void mousePressEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + + void mouseMoveEvent(QMouseEvent *event) override; private: bool lockScroll; + QTimer *blinkTimer; + bool cursorVisible = true; + QColor cursorColor; + + DisassemblyWidget *disasmWidget = nullptr; }; /** diff --git a/src/widgets/GraphView.cpp b/src/widgets/GraphView.cpp index 2aa66a9f..e7c6ba11 100644 --- a/src/widgets/GraphView.cpp +++ b/src/widgets/GraphView.cpp @@ -349,13 +349,16 @@ void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, b p.setBrush(Qt::gray); - // Always draw edges - // TODO: Only draw edges if they are actually visible ... // Draw edges for (GraphEdge &edge : block.edges) { if (edge.polyline.empty()) { continue; } + // Skips expensive polyline and arrow rendering if the edge's bounding box + // is completely outside the current viewport. + if (!edge.polyline.boundingRect().adjusted(-1, -1, 1, 1).intersects(windowF)) { + continue; + } const QPolygonF polyline = edge.polyline; const EdgeConfiguration ec = edgeConfiguration(block, &blocks[edge.target], interactive);