diff --git a/CMakeLists.txt b/CMakeLists.txt index e785237fc0..954bf8df7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ set("CUTTER_QT" 6 CACHE STRING "Major QT version to use 5|6") set_property(CACHE "CUTTER_QT" PROPERTY STRINGS 5 6) option(CUTTER_INCLUDE_GIT_HASH "Include git hash in full version" ON) set(CUTTER_VERSION_SUFFIX "" CACHE STRING "Can be used by packagers to differentiate multiple packages using same source, for example build number or presence of additional patches.") +option(CUTTER_ASAN "Build with ASAN" OFF) if(NOT CUTTER_ENABLE_PYTHON) set(CUTTER_ENABLE_PYTHON_BINDINGS OFF) @@ -186,11 +187,13 @@ message(STATUS "- Package RzSilhouette: ${CUTTER_PACKAGE_RZ_SILHOUETTE}") message(STATUS "- Package JSDec: ${CUTTER_PACKAGE_JSDEC}") message(STATUS "- QT: ${CUTTER_QT}") message(STATUS "") +message(STATUS "- ASAN: ${CUTTER_ASAN}") if (CUTTER_QT LESS 5 OR CUTTER_QT GREATER 6) message(FATAL_ERROR "Unsupported QT major version") endif() + add_subdirectory(src) if(CUTTER_ENABLE_PACKAGING) diff --git a/cmake/BundledRizin.cmake b/cmake/BundledRizin.cmake index b89399b417..e51d10f6ef 100644 --- a/cmake/BundledRizin.cmake +++ b/cmake/BundledRizin.cmake @@ -22,6 +22,11 @@ if (CUTTER_ENABLE_SIGDB) list(APPEND MESON_OPTIONS "-Dinstall_sigdb=true") endif() +# Enable ASan/UBSan in bundled Rizin when Cutter ASAN is enabled +if(CUTTER_ASAN) + list(APPEND MESON_OPTIONS "-Db_sanitize=address,undefined") +endif() + find_program(MESON meson) if(NOT MESON) message(FATAL_ERROR "Failed to find meson, which is required to build bundled rizin") @@ -85,4 +90,4 @@ if (WIN32) else () install(DIRECTORY "${RIZIN_INSTALL_DIR}/" DESTINATION "." USE_SOURCE_PERMISSIONS PATTERN "rz-test" EXCLUDE) -endif() +endif() \ No newline at end of file diff --git a/rizin b/rizin index da228d11cf..c351d5f32a 160000 --- a/rizin +++ b/rizin @@ -1 +1 @@ -Subproject commit da228d11cfc865b06442bd66b192caeb6e5ff556 +Subproject commit c351d5f32a5c17a61a6ff7931ab6cb5059a3d1e5 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fba081a05..313cc38fe1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -174,6 +174,13 @@ set(SOURCES dialogs/preferences/RizinConfigOptionsWidget.cpp dialogs/IntervalDialog.cpp dialogs/StringListDialog.cpp + tools/bindiff/DiffLoadDialog.cpp + core/BinDiff.cpp + core/CutterDiff.cpp + tools/bindiff/CutterDiffWindow.cpp + tools/bindiff/DiffWaitDialog.cpp + tools/bindiff/HexDiff.cpp + tools/bindiff/LineDiffWidget.cpp ) set(HEADER_FILES core/Cutter.h @@ -356,6 +363,13 @@ set(HEADER_FILES dialogs/preferences/RizinConfigOptionsWidget.h dialogs/IntervalDialog.h dialogs/StringListDialog.h + tools/bindiff/DiffLoadDialog.h + core/BinDiff.h + core/CutterDiff.h + tools/bindiff/CutterDiffWindow.h + tools/bindiff/DiffWaitDialog.h + tools/bindiff/HexDiff.h + tools/bindiff/LineDiffWidget.h ) set(UI_FILES dialogs/AboutDialog.ui @@ -440,6 +454,10 @@ set(UI_FILES dialogs/preferences/SymbolsOptionsWidget.ui dialogs/preferences/InterfaceOptionsWidget.ui dialogs/preferences/RizinConfigOptionsWidget.ui + tools/bindiff/DiffLoadDialog.ui + tools/bindiff/CutterDiffWindow.ui + tools/bindiff/DiffWaitDialog.ui + tools/bindiff/LineDiffWidget.ui ) set(QRC_FILES resources.qrc @@ -592,6 +610,12 @@ if(CUTTER_ENABLE_PYTHON) endif() endif() +if(CUTTER_ASAN) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdiagnostics-color=always -fsanitize=address,undefined -fno-omit-frame-pointer -fsanitize-address-use-after-scope") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always -fsanitize=address,undefined -fno-omit-frame-pointer -fsanitize-address-use-after-scope") + set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fdiagnostics-color=always -fsanitize=address,undefined -fno-omit-frame-pointer -fsanitize-address-use-after-scope") +endif() + if(TARGET KF${CUTTER_QT}::SyntaxHighlighting) target_link_libraries(Cutter PRIVATE KF${CUTTER_QT}::SyntaxHighlighting) target_compile_definitions(Cutter PRIVATE CUTTER_ENABLE_KSYNTAXHIGHLIGHTING) @@ -602,11 +626,11 @@ if (CUTTER_APPIMAGE_BUILD) endif() if (CUTTER_PACKAGE_JSDEC) - target_compile_definitions(Cutter PRIVATE CUTTER_BUNDLE_JSDEC) + target_compile_definitions(Cutter PRIVATE CUTTER_BUNDLE_JSDEC) endif() if(APPLE AND CUTTER_ENABLE_PACKAGING AND CUTTER_USE_BUNDLED_RIZIN) - target_compile_definitions(Cutter PRIVATE MACOS_RZ_BUNDLED) + target_compile_definitions(Cutter PRIVATE MACOS_RZ_BUNDLED) endif() if(CUTTER_ENABLE_PACKAGING) diff --git a/src/core/BinDiff.cpp b/src/core/BinDiff.cpp new file mode 100644 index 0000000000..eb787b7c6d --- /dev/null +++ b/src/core/BinDiff.cpp @@ -0,0 +1,210 @@ +#include "BinDiff.h" + +bool BinDiff::threadCallback(const size_t nLeft, const size_t nMatch, void *user) +{ + auto bdiff = reinterpret_cast(user); + return bdiff->updateProgress(nLeft, nMatch); +} + +BinDiff::BinDiff() + : result(nullptr), + continueRun(true), + maxTotal(1) +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + , + mutex(QMutex::Recursive) +#endif +{ + cutterDiff.reset(new CutterDiff()); +} + +BinDiff::~BinDiff() +{ + rz_analysis_match_result_free(result); +} + +bool BinDiff::hasData() +{ + return result != nullptr; +} + +void BinDiff::setFileA(QString filePath) +{ + mutex.lock(); + fileA = std::move(filePath); + mutex.unlock(); +} + +void BinDiff::setFileB(QString filePath) +{ + mutex.lock(); + fileB = std::move(filePath); + mutex.unlock(); +} + +void BinDiff::setAnalysisLevel(int aLevel) +{ + mutex.lock(); + level = aLevel; + mutex.unlock(); +} + +void BinDiff::setCompareLogic(int cLogic) +{ + mutex.lock(); + compareLogic = cLogic; + mutex.unlock(); +} + +void BinDiff::run() +{ + qRegisterMetaType(); + + mutex.lock(); + rz_analysis_match_result_free(result); + continueRun = true; + maxTotal = 1; // maxTotal must be at least 1. + mutex.unlock(); + cutterDiff->initCores(); + cutterDiff->syncConfig(); + cutterDiff->openFiles(fileA, fileB); + cutterDiff->analyzeCores(level); + result = cutterDiff->matchFunctions(compareLogic, threadCallback, this); + sortFunctions(); + mutex.lock(); + const bool canComplete = continueRun; + mutex.unlock(); + if (canComplete) { + emit complete(); + } +} + +void BinDiff::cancel() +{ + mutex.lock(); + continueRun = false; + mutex.unlock(); +} + +static void setFunctionDescription(FunctionDescription *desc, const RzAnalysisFunction *func) +{ + desc->offset = func->addr; + desc->linearSize = rz_analysis_function_linear_size(const_cast(func)); + desc->nargs = rz_analysis_arg_count(const_cast(func)); + desc->nlocals = rz_analysis_var_local_count(const_cast(func)); + desc->nbbs = rz_pvector_len(func->bbs); + desc->calltype = func->cc ? QString::fromUtf8(func->cc) : QString(); + desc->name = func->name ? QString::fromUtf8(func->name) : QString(); + desc->edges = rz_analysis_function_count_edges(func, nullptr); + desc->stackframe = func->maxstack; +} + +struct MatchEntry +{ + const RzAnalysisFunction *fcnA; + const RzAnalysisFunction *fcnB; + double similarity; +}; + +void BinDiff::sortFunctions() +{ + if (!result) { + return; + } + + matchedList.clear(); + removedSet.clear(); + addedSet.clear(); + + QHash bestMatches; + QSet discardedA; + + const RzAnalysisMatchPair *pair = nullptr; + const RzListIter *it = nullptr; + + CutterRzListForeach (result->matches, it, RzAnalysisMatchPair, pair) { + + auto *fcnA = static_cast(pair->pair_a); + auto *fcnB = static_cast(pair->pair_b); + + auto hashIt = bestMatches.find(fcnB); + + if (hashIt == bestMatches.end()) { + bestMatches.insert(fcnB, { fcnA, fcnB, pair->similarity }); + continue; + } + + if (pair->similarity > hashIt->similarity) { + + discardedA.insert(hashIt->fcnA); + + discardedA.remove(fcnA); + + hashIt->fcnA = fcnA; + hashIt->similarity = pair->similarity; + } else { + + discardedA.insert(fcnA); + } + } + + for (const auto &entry : std::as_const(bestMatches)) { + + BinDiffMatchDescription desc; + + setFunctionDescription(&desc.original, entry.fcnA); + setFunctionDescription(&desc.modified, entry.fcnB); + + desc.similarity = entry.similarity; + desc.simtype = RZ_ANALYSIS_SIMILARITY_TYPE_STR(entry.similarity); + + matchedList.push_back(std::move(desc)); + } + removedSet = std::move(discardedA); + + const RzAnalysisFunction *func = nullptr; + + CutterRzListForeach (result->unmatch_a, it, RzAnalysisFunction, func) { + removedSet.insert(func); + } + CutterRzListForeach (result->unmatch_b, it, RzAnalysisFunction, func) { + addedSet.insert(func); + } +} + +QList BinDiff::matches() +{ + return matchedList; +} + +QList BinDiff::mismatch(bool originalFile) +{ + QList list; + for (const RzAnalysisFunction *func : (originalFile ? removedSet : addedSet)) { + FunctionDescription desc; + setFunctionDescription(&desc, func); + list.push_back(desc); + } + return list; +} + +bool BinDiff::updateProgress(size_t nLeft, size_t nMatch) +{ + mutex.lock(); + if (nMatch > maxTotal) { + maxTotal = nMatch; + } + if (nLeft > maxTotal) { + maxTotal = nLeft; + } + + BinDiffStatusDescription status; + status.total = maxTotal; + status.nLeft = nLeft; + status.nMatch = nMatch; + + emit progress(status); + const bool ret = continueRun; + mutex.unlock(); + return ret; +} diff --git a/src/core/BinDiff.h b/src/core/BinDiff.h new file mode 100644 index 0000000000..ed07ef8f79 --- /dev/null +++ b/src/core/BinDiff.h @@ -0,0 +1,69 @@ +#ifndef CUTTER_BINDIFF_CORE_H +#define CUTTER_BINDIFF_CORE_H + +#include "Cutter.h" +#include "CutterDescriptions.h" +#include "CutterDiff.h" + +#include +#include + +#include + +class CutterDiffWindow; +/** + * @brief The BinDiff class + * Thread run for processing the functional diffing and other large diffing processes. + * TODO: Move to common or tools/bindiff? + */ +class BinDiff : public QThread +{ + Q_OBJECT + friend class CutterDiffWindow; + +public: + explicit BinDiff(); + virtual ~BinDiff(); + + void run(); + + void setFileA(QString filePth); + void setFileB(QString filePth); + void setAnalysisLevel(int aLevel); + void setCompareLogic(int cLogic); + bool hasData(); + + QList matches(); + QList mismatch(bool originalFile); + +public slots: + void cancel(); + +signals: + void progress(BinDiffStatusDescription status); + void complete(); + +private: + std::unique_ptr cutterDiff; + RzAnalysisMatchResult *result; + void sortFunctions(); + QList matchedList; + QSet removedSet; + QSet addedSet; + bool continueRun; + size_t maxTotal; +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QMutex mutex; +#else + QRecursiveMutex mutex; +#endif + QString fileA; + QString fileB; + int level; + int compareLogic; + + bool updateProgress(const size_t nLeft, const size_t nMatch); + static bool threadCallback(const size_t nLeft, const size_t nMatch, void *user); +}; + +#endif // CUTTER_BINDIFF_CORE_H diff --git a/src/core/Cutter.h b/src/core/Cutter.h index f57f2eabd7..14b37caa3f 100644 --- a/src/core/Cutter.h +++ b/src/core/Cutter.h @@ -145,7 +145,7 @@ class CUTTER_EXPORT CutterCore : public QObject /** * @brief a wrapper around cmdRaw(const char *cmd,). */ - QString cmdRaw(const QString &cmd) { return cmdRaw(cmd.toUtf8().constData()); }; + QString cmdRaw(const QString &cmd) { return cmdRaw(cmd.toUtf8().constData()); } /** * @brief Execute a Rizin command \a cmd at \a address. The function will preform a silent seek diff --git a/src/core/CutterDescriptions.h b/src/core/CutterDescriptions.h index 0f23398b04..a901034708 100644 --- a/src/core/CutterDescriptions.h +++ b/src/core/CutterDescriptions.h @@ -423,6 +423,21 @@ struct BasefindResultDescription quint32 score; }; +struct BinDiffMatchDescription +{ + FunctionDescription original; + FunctionDescription modified; + QString simtype; + double similarity; +}; + +struct BinDiffStatusDescription +{ + size_t total; + size_t nLeft; + size_t nMatch; +}; + struct MarkDescription { RVA from; @@ -496,6 +511,8 @@ Q_DECLARE_METATYPE(RefDescription) Q_DECLARE_METATYPE(VariableDescription) Q_DECLARE_METATYPE(BasefindCoreStatusDescription) Q_DECLARE_METATYPE(BasefindResultDescription) +Q_DECLARE_METATYPE(BinDiffMatchDescription) +Q_DECLARE_METATYPE(BinDiffStatusDescription) Q_DECLARE_METATYPE(MarkDescription) Q_DECLARE_METATYPE(BacktraceDescription) Q_DECLARE_METATYPE(EvaluableVarDescription) diff --git a/src/core/CutterDiff.cpp b/src/core/CutterDiff.cpp new file mode 100644 index 0000000000..1d81911579 --- /dev/null +++ b/src/core/CutterDiff.cpp @@ -0,0 +1,506 @@ +#include "CutterDiff.h" + +#include "Configuration.h" + +#include + +#define LOCK() const CutterDiffLocked lock(this); + +CutterDiff::CutterDiff(QObject *parent) + : QObject { parent } +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + , + mutex(QMutex::Recursive) +#endif +{ +} + +CutterDiff::~CutterDiff() +{ + rz_core_free(coreA); + rz_core_free(coreB); +} + +bool CutterDiff::initCores() +{ + LOCK(); + + if (coreA || coreB) { + qInfo() << "preInit"; + } + + coreA = rz_core_new(); + coreB = rz_core_new(); + + // reassigning console to the main cutter core till the context based console is ready + { + RzCoreLocked core(Core()); + core->cons->line->user = core; + core->cons->line->cb_fkey = core->cons->cb_fkey; + core->cons->user_fgets_user = core; + rz_core_bind_cons(core); + } + + if (!(coreA || coreB)) { + goto fail; + } + + rz_core_loadlibs(coreA, RZ_CORE_LOADLIBS_ALL); + rz_core_loadlibs(coreB, RZ_CORE_LOADLIBS_ALL); + + coreA->print->scr_prompt = false; + coreB->print->scr_prompt = false; + + return true; +fail: + qWarning() << "Core initialization has failed undefined behaviour expected."; + rz_core_free(coreA); + rz_core_free(coreB); + return false; +} + +bool CutterDiff::openFiles(const QString &fileA, const QString &fileB) +{ + + LOCK(); + // open core files + if (!rz_core_file_open(coreA, fileA.toUtf8().constData(), RZ_PERM_RX, 0)) { + qWarning() << tr("cannot open file %1").arg(fileA); + goto fail; + } + + if (!rz_core_file_open(coreB, fileB.toUtf8().constData(), RZ_PERM_RX, 0)) { + qWarning() << tr("cannot open file %1").arg(fileB); + goto fail; + } + + if (!rz_core_bin_load(coreA, nullptr, UT64_MAX)) { + qWarning() << tr("cannot load bin %1").arg(fileA); + goto fail; + } + + if (!rz_core_bin_load(coreB, nullptr, UT64_MAX)) { + qWarning() << tr("cannot load bin %1").arg(fileB); + goto fail; + } + + if (!rz_core_bin_update_arch_bits(coreA)) { + qWarning() << tr("cannot set architecture with bits in fileA"); + goto fail; + } + + if (!rz_core_bin_update_arch_bits(coreB)) { + qWarning() << tr("cannot set architecture with bits in fileB"); + goto fail; + } + filePathA = fileA; + filePathB = fileB; + fileNameA = QFileInfo(filePathA).fileName(); + fileNameB = QFileInfo(filePathB).fileName(); + return true; +fail: + rz_core_file_close_all_but(coreA); + rz_core_file_close_all_but(coreB); + return false; +} + +void CutterDiff::syncConfig() +{ + RzConfigEntry *var; + RzConfigNode *node; + RzCoreLocked core(Core()); + CutterRzVectorForeach(&core->config->sorted_vars, var, RzConfigEntry) + { + node = &var->node; + if (!strcmp(node->name, "scr.color") || !strcmp(node->name, "scr.interactive") + || !strcmp(node->name, "cfg.debug")) { + rz_config_set(coreA->config, node->name, "0"); + rz_config_set(coreB->config, node->name, "0"); + continue; + } + rz_config_set(coreA->config, node->name, node->value); + rz_config_set(coreB->config, node->name, node->value); + } +} + +bool CutterDiff::analyzeCores(int level) +{ + LOCK(); + if (!rz_core_analysis_all(coreA)) { + qWarning() << tr("cannot perform basic analysis of the binary fileA"); + goto fail; + } + if (!rz_core_analysis_all(coreB)) { + qWarning() << tr("cannot perform basic analysis of the binary fileB"); + goto fail; + } + + if (level != AnalysisLevelSymbols + && !rz_core_analysis_everything(coreA, level == AnalysisLevelExperimental, nullptr)) { + qWarning() << tr("cannot perform complete analysis of the binaryA"); + goto fail; + } + + if (level != AnalysisLevelSymbols + && !rz_core_analysis_everything(coreB, level == AnalysisLevelExperimental, nullptr)) { + qWarning() << tr("cannot perform complete analysis of the binaryB"); + goto fail; + } + return true; +fail: + return false; +} + +QList CutterDiff::getFunctionList(bool orig) +{ + LOCK(); + QList list; + const RzList *functions = rz_analysis_function_list(orig ? coreA->analysis : coreB->analysis); + if (!functions) { + return list; + } + RzAnalysisFunction *func = nullptr; + const RzListIter *it = nullptr; + CutterRzListForeach (functions, it, RzAnalysisFunction, func) { + FunctionDescription desc; + desc.offset = func->addr; + desc.linearSize = rz_analysis_function_linear_size(const_cast(func)); + desc.nargs = rz_analysis_arg_count(const_cast(func)); + desc.nlocals = rz_analysis_var_local_count(const_cast(func)); + desc.nbbs = rz_pvector_len(func->bbs); + desc.calltype = func->cc ? QString::fromUtf8(func->cc) : QString(); + desc.name = func->name ? QString::fromUtf8(func->name) : QString(); + desc.edges = rz_analysis_function_count_edges(func, nullptr); + desc.stackframe = func->maxstack; + list.push_back(desc); + } + return list; +} + +#define IS_IMPORT(name) \ + (name.startsWith("sym.imp.") || name.startsWith("loc.imp.") || name.startsWith("imp.")) +#define IS_SYMBOL(name, pfx) (IS_IMPORT(name) || name.startsWith(pfx)) + +RzList *CutterDiff::getFunctions(RzAnalysis *analysis, int compareLogic) +{ + LOCK(); + const RzList *functions = rz_analysis_function_list(analysis); + if (!functions) { + return nullptr; + } + + RzList *list = rz_list_newf(nullptr); + if (!list) { + return list; + } + + QString pfx = Config()->getConfigString("analysis.fcnprefix"); + if (pfx.isEmpty()) { + pfx = "fcn."; + } else { + pfx += "."; + } + + RzAnalysisFunction *func = nullptr; + const RzListIter *it = nullptr; + + CutterRzListForeach (functions, it, RzAnalysisFunction, func) { + const QString name = func->name; + if (compareLogic == CutterDiff::CompareLogicDefault && IS_IMPORT(name)) { + continue; + } else if (compareLogic == CutterDiff::CompareLogicSymbols && IS_SYMBOL(name, pfx)) { + continue; + } + rz_list_add_sorted(list, func, rz_analysis_get_column_sort(analysis), nullptr); + } + + return list; +} + +QByteArray CutterDiff::ioRead(RVA addr, int len, bool orig) +{ + LOCK(); + const RzCore *core = orig ? coreA : coreB; + QByteArray array; + + if (len <= 0) { + return array; + } + + /* Zero-copy */ + array.resize(len); + if (!core || !core->io + || !rz_io_read_at_mapped(core->io, addr, reinterpret_cast(array.data()), len)) { + array.fill(0xff); + } + + return array; +} + +RzAnalysisMatchResult *CutterDiff::matchFunctions(int compareLogic, + RzAnalysisMatchThreadInfoCb callback, void *user) +{ + LOCK(); + RzList *fcnsA = nullptr, *fcnsB = nullptr; + RzAnalysisMatchResult *result = nullptr; + RzAnalysisMatchOpt opts; + + fcnsA = getFunctions(coreA->analysis, compareLogic); + if (rz_list_empty(fcnsA)) { + qWarning() << tr("no functions found in the current opened file"); + goto fail; + } + + fcnsB = getFunctions(coreB->analysis, compareLogic); + if (rz_list_empty(fcnsB)) { + qWarning() << tr("no functions found in the just opene file %1"); + goto fail; + } + + opts.analysis_a = coreA->analysis; + opts.analysis_b = coreB->analysis; + opts.callback = callback; + opts.user = user; + + // calculate all the matches between the functions of the 2 different core files. + result = rz_analysis_match_functions(fcnsA, fcnsB, &opts); + if (!result) { + qWarning() << tr("failed to perform the function matching operation or job was cancelled."); + goto fail; + } + + rz_list_free(fcnsA); + rz_list_free(fcnsB); + return result; + +fail: + + rz_list_free(fcnsA); + rz_list_free(fcnsB); + // rz_core_file_close_all_but(diffCore); + return nullptr; +} + +QString CutterDiff::getCommentAt(RVA addr, bool orig) +{ + LOCK(); + const RzCore *core = orig ? coreA : coreB; + if (!core->analysis) { + return ""; + } + return rz_meta_get_string(core->analysis, RZ_META_TYPE_COMMENT, addr); +} + +QString CutterDiff::listFlagsAsStringAt(RVA addr, bool orig) +{ + LOCK(); + const RzCore *core = orig ? coreA : coreB; + if (!core || !core->flags) { + return ""; + } + char *flagList = rz_flag_get_liststr(core->flags, addr); + QString result = fromOwnedCharPtr(flagList); + return result; +} + +bool CutterDiff::cmpBytesAt(RVA addrA, RVA addrB, size_t len) +{ + LOCK(); + if (len <= 0) { + return true; + } + if (!coreA || !coreB || !coreA->io || !coreB->io) { + qWarning() << "Cores not initialized"; + return false; + } + QByteArray bufA; + bufA.resize(len); + QByteArray bufB; + bufB.resize(len); + + if (!rz_io_read_at_mapped(coreA->io, addrA, reinterpret_cast(bufA.data()), len) + || !rz_io_read_at_mapped(coreB->io, addrB, reinterpret_cast(bufB.data()), len)) { + qWarning() << "Read failed"; + return false; + }; + return bufA == bufB; +} + +QString CutterDiff::getCommonConfig(const char *k) +{ + LOCK(); + return { rz_config_get(coreA->config, k) }; +} + +void CutterDiff::setCommonConfig(const char *k, const char *v) +{ + LOCK(); + rz_config_set(coreA->config, k, v); + rz_config_set(coreB->config, k, v); +} + +int CutterDiff::getCommonConfigi(const char *k) +{ + LOCK(); + return static_cast(rz_config_get_i(coreA->config, k)); +} + +bool CutterDiff::getCommonConfigb(const char *k) +{ + LOCK(); + return rz_config_get_b(coreA->config, k); +} + +void CutterDiff::setCommonConfigb(const char *k, bool value) +{ + LOCK(); + rz_config_set_b(coreA->config, k, value); + rz_config_set_b(coreB->config, k, value); +} + +void CutterDiff::setCommonConfigi(const char *k, int v) +{ + LOCK(); + rz_config_set_i(coreA->config, k, static_cast(v)); + rz_config_set_i(coreB->config, k, static_cast(v)); +} + +void CutterDiff::seekSilent(ut64 offset, bool orig) +{ + LOCK(); + if (offset == RVA_INVALID) { + return; + } + rz_core_seek(orig ? coreA : coreB, offset, true); +} + +QString CutterDiff::cmdRawAt(const char *cmd, RVA address, bool orig) +{ + LOCK(); + QString res; + const RVA oldOffset = getOffset(orig); + seekSilent(address, orig); + + res = cmdRaw(cmd, orig); + + seekSilent(oldOffset, orig); + return res; +} + +QString CutterDiff::cmdRaw(const char *cmd, bool orig) +{ + LOCK(); + const QString res; + return rz_core_cmd_str(orig ? coreA : coreB, cmd); +} + +RVA CutterDiff::getOffset(bool orig) +{ + return (orig ? coreA : coreB)->offset; +} + +QString CutterDiff::ansiEscapeToHtml(const QString &text) +{ + int len; + QString r = text; + r.replace("\t", " "); + char *html = rz_cons_html_filter( + r.toUtf8().constData(), + &len); // doesn't support background colors work has to be done in the rizin side + if (!html) { + return {}; + } + r = QString::fromUtf8(html, len); + rz_mem_free(html); + return r; +} + +QString CutterDiff::disassembleFunction(RVA addr, bool orig) +{ + LOCK(); + RzCore *core = orig ? coreA : coreB; + RzAnalysisFunction *function = rz_analysis_get_fcn_in( + core->analysis, addr, RZ_ANALYSIS_FCN_TYPE_FCN | RZ_ANALYSIS_FCN_TYPE_SYM); + if (!function) { + qWarning() << QString("Could not load function at %1").arg(QString::number(addr, 16)); + return {}; + } + auto vec = fromOwned( + rz_pvector_new(reinterpret_cast(rz_analysis_disasm_text_free))); + if (!vec) { + return {}; + } + const uint64_t start = function->addr; + const uint64_t end = rz_analysis_function_max_addr(function); + if (start > end) { + qWarning() << "Start address is greater than end address of the function"; + return {}; + } + const uint64_t size = end - start; + QByteArray array; + array.resize(size); + rz_io_read_at_mapped(core->io, start, reinterpret_cast(array.data()), size); + RzCoreDisasmOptions disasmOptions = { .cbytes = 1, .function = function, .vec = vec.get() }; + TempDiffConfig config(this, orig); + config.setConfigi("scr.utf8", 0); + config.setConfigi("asm.offset", 0); + config.setConfigi("asm.lines", 0); + config.setConfigi("asm.cmt.right", 0); + config.setConfigi("asm.lines.fcn", 0); + config.setConfigi("asm.bytes", 0); + config.setConfigi("asm.comments", 0); + config.setConfigi("scr.color", COLOR_MODE_DISABLED); + rz_core_print_disasm(core, start, reinterpret_cast(array.data()), size, size, nullptr, + &disasmOptions); + QString r; + for (const auto &t : CutterPVector(vec.get())) { + const QString text = t->text; + r.append(text); + r.append("\n"); + } + return r; +} + +RzDiff *CutterDiff::diffFunctionDissas(RVA addrA, RVA addrB) +{ + LOCK(); + const QString disasA = disassembleFunction(addrA, true); + const QString disasB = disassembleFunction(addrB, false); + return lineDiff(disasA, disasB); +} + +RzDiff *CutterDiff::lineDiff(const char *lines1, const char *lines2) +{ + RzDiff *diff = rz_diff_lines_new(lines1, lines2, nullptr); + return diff; +} + +RzDiff *CutterDiff::lineDiff(const QString &lines1, const QString &lines2) +{ + return lineDiff(lines1.toUtf8().constData(), lines2.toUtf8().constData()); +} + +CutterRzList*/> CutterDiff::lineDiffOpsGrouped(RzDiff *diff) const +{ + auto *groups = rz_diff_opcodes_grouped_new(diff, 2); + return CutterRzList*/>(groups); +} + +Bound CutterDiff::getLineDiffBounds(const QString &line1, const QString &line2) +{ + if (line1.size() != line2.size()) { + return { 0, 0 }; + } + + int first = 0; + while (first < line1.size() && line1[first] == line2[first]) { + ++first; + } + + int last = line1.size() - 1; + while (last >= first && line1[last] == line2[last]) { + --last; + } + + return { first, last - first + 1 }; +} diff --git a/src/core/CutterDiff.h b/src/core/CutterDiff.h new file mode 100644 index 0000000000..90bad93359 --- /dev/null +++ b/src/core/CutterDiff.h @@ -0,0 +1,193 @@ +#ifndef CUTTERDIFF_H +#define CUTTERDIFF_H + +#include "Cutter.h" +#include "CutterCommon.h" +#include "RizinCpp.h" + +#include +#include +#include + +class CutterDiffLocked; + +struct Bound +{ + int pos; + int size; +}; + +class CUTTER_EXPORT CutterDiff : public QObject +{ + Q_OBJECT + friend class CutterDiffLocked; + +public: + explicit CutterDiff(QObject *parent = nullptr); + ~CutterDiff(); + bool initCores(); + bool openFiles(const QString &fileA, const QString &fileB); + bool analyzeCores(int level); + void syncConfig(); + RzAnalysisMatchResult *matchFunctions(int compareLogic, RzAnalysisMatchThreadInfoCb callback, + void *user); + enum : ut8 { AnalysisLevelSymbols, AnalysisLevelAuto, AnalysisLevelExperimental }; + + enum : ut8 { + CompareLogicDefault = 0, ///< Only symbols and functions (no imports) + CompareLogicComplete, ///< All functions (imports included) + CompareLogicSymbols, ///< Only symbols + }; + QList getFunctionList(bool orig = true); + QByteArray ioRead(RVA addr, int len, bool orig = true); + QString getCommentAt(RVA addr, bool orig = true); + QString listFlagsAsStringAt(RVA addr, bool orig = true); + bool cmpBytesAt(RVA addrA, RVA addrB, size_t len); + QString getString(RVA addr, uint64_t len, RzStrEnc encoding, bool escape_nl = false, + bool orig = true); + QString getCommonConfig(const char *key); + QString getCommonConfig(const QString &key) + { + return getCommonConfig(key.toUtf8().constData()); + } + void setCommonConfig(const char *key, const char *value); + int getCommonConfigi(const char *key); + int getCommonConfigi(const QString &key) { return getCommonConfigi(key.toUtf8().constData()); } + bool getCommonConfigb(const char *key); + bool getCommonConfigb(const QString &key) { return getCommonConfigb(key.toUtf8().constData()); } + void setCommonConfigi(const char *key, int value); + void setCommonConfigb(const char *key, bool value); + void seekSilent(ut64 offset, bool orig); + QString cmdRawAt(const char *cmd, RVA address, bool orig); + QString cmdRaw(const char *cmd, bool orig); + RVA getOffset(bool orig); + RzDiff *lineDiff(const char *lines1, const char *lines2); // Own RzDiff + RzDiff *lineDiff(const QString &lines1, const QString &lines2); // Own RzDiff + RzDiff *diffFunctionDissas(RVA addrA, RVA addrB); // Own RzDiff + QString diffFunctionDecomp(RVA addrA, RVA addrB); + QString diffFunctionRzIL(RVA addrA, RVA addrB); + QString disassembleFunction(RVA addr, bool orig); + QString ansiEscapeToHtml(const QString &text); + CutterRzList*/> lineDiffOpsGrouped(RzDiff *diff) const; + QString getFileName(bool orig = true) const { return orig ? fileNameA : fileNameB; } + QString getFilePath(bool orig = true) const { return orig ? filePathA : filePathB; } + Bound getLineDiffBounds(const QString &line1, const QString &line2); + +private: + RzCore *coreA = nullptr; + RzCore *coreB = nullptr; + QString fileNameA; + QString fileNameB; + QString filePathA; + QString filePathB; +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) + QMutex mutex; +#else + QRecursiveMutex mutex; +#endif + RzList *getFunctions(RzAnalysis *analysis, int compareLogic); // Own RzList +signals: +}; + +class CutterDiffLocked +{ + CutterDiff *const diff; + +public: + RzCore *const coreA; + RzCore *const coreB; + explicit CutterDiffLocked(CutterDiff *diff) : diff(diff), coreA(diff->coreA), coreB(diff->coreB) + { + assert(diff); + diff->mutex.lock(); + } + ~CutterDiffLocked() { diff->mutex.unlock(); } + CutterDiff *operator->() & { return diff; } + CutterDiff *operator->() const & { return diff; } + CutterDiff *operator->() && = delete; +}; + +class TempDiffConfig +{ +public: + explicit TempDiffConfig(CutterDiff *cutterDiff, bool orig = true) + : cutterDiff(cutterDiff), orig(orig) + { + } + + ~TempDiffConfig() + { + const CutterDiffLocked lock(cutterDiff); + const RzCore *const core = orig ? lock.coreA : lock.coreB; + + for (auto it = iHash.cbegin(); it != iHash.cend(); ++it) { + const QByteArray key = it.key().toUtf8(); + rz_config_set_i(core->config, key.constData(), it.value()); + } + + for (auto it = bHash.cbegin(); it != bHash.cend(); ++it) { + const QByteArray key = it.key().toUtf8(); + rz_config_set_b(core->config, key.constData(), it.value()); + } + + for (auto it = cHash.cbegin(); it != cHash.cend(); ++it) { + const QByteArray key = it.key().toUtf8(); + const QByteArray value = it.value().toUtf8(); + rz_config_set(core->config, key.constData(), value.constData()); + } + } + + void setConfigi(const QString &config, ut64 val) + { + const CutterDiffLocked lock(cutterDiff); + const RzCore *const core = orig ? lock.coreA : lock.coreB; + + const QByteArray key = config.toUtf8(); + + if (!iHash.contains(config)) { + iHash.insert(config, rz_config_get_i(core->config, key.constData())); + } + + rz_config_set_i(core->config, key.constData(), val); + } + + void setConfigb(const QString &config, bool val) + { + const CutterDiffLocked lock(cutterDiff); + const RzCore *const core = orig ? lock.coreA : lock.coreB; + + const QByteArray key = config.toUtf8(); + + if (!bHash.contains(config)) { + bHash.insert(config, rz_config_get_b(core->config, key.constData())); + } + + rz_config_set_b(core->config, key.constData(), val); + } + + void setConfig(const QString &config, const QString &val) + { + const CutterDiffLocked lock(cutterDiff); + const RzCore *const core = orig ? lock.coreA : lock.coreB; + + const QByteArray key = config.toUtf8(); + + if (!cHash.contains(config)) { + const char *oldValue = rz_config_get(core->config, key.constData()); + cHash.insert(config, oldValue ? QString::fromUtf8(oldValue) : QString()); + } + + const QByteArray value = val.toUtf8(); + rz_config_set(core->config, key.constData(), value.constData()); + } + +private: + CutterDiff *cutterDiff; + const bool orig; + + QHash iHash; + QHash bHash; + QHash cHash; +}; + +#endif // CUTTERDIFF_H diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index e72be732d4..c6f7a51469 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -115,6 +115,7 @@ // Tools #include "tools/basefind/BaseFindDialog.h" +#include "tools/bindiff/DiffLoadDialog.h" template T *getNewInstance(MainWindow *m) @@ -400,6 +401,8 @@ void MainWindow::initToolBar() connect(ui->actionResetSettings, &QAction::triggered, this, &MainWindow::onActionResetSettingsTriggered); connect(ui->actionTheme, &QAction::triggered, this, &MainWindow::chooseThemeIcons); + connect(ui->actionDiffFiles, &QAction::triggered, this, + &MainWindow::onActionDiffFilesTriggered); } void MainWindow::initDocks() @@ -1747,6 +1750,12 @@ void MainWindow::onActionRefreshPanelsTriggered() this->refreshAll(); } +void MainWindow::onActionDiffFilesTriggered() +{ + auto *diffLoad = new DiffLoadDialog(this); + diffLoad->show(); +} + void MainWindow::onActionAnalyzeTriggered() const { auto *analysisTask = new AnalysisTask(); diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index b336e4858a..12615dcec2 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -184,6 +184,8 @@ private slots: void onActionDefaultTriggered(); + void onActionDiffFilesTriggered(); + /** * @brief MainWindow::on_actionNew_triggered * Open a new Cutter session. diff --git a/src/core/MainWindow.ui b/src/core/MainWindow.ui index 5661bfe342..6ab457ceac 100644 --- a/src/core/MainWindow.ui +++ b/src/core/MainWindow.ui @@ -51,9 +51,9 @@ - 2024 - 127 - 196 + 340 + 86 + 206 389 @@ -133,6 +133,7 @@ &Tools + @@ -878,6 +879,11 @@ Manage layouts + + + Diff Files + + diff --git a/src/tools/bindiff/CutterDiffWindow.cpp b/src/tools/bindiff/CutterDiffWindow.cpp new file mode 100644 index 0000000000..44171fc011 --- /dev/null +++ b/src/tools/bindiff/CutterDiffWindow.cpp @@ -0,0 +1,732 @@ +#include "CutterDiffWindow.h" + +#include "DiffLoadDialog.h" +#include "ui_CutterDiffWindow.h" + +#include +#include + +#include + +FunctionListModel::FunctionListModel(QList *list, QObject *parent) + : AddressableItemModel<>(parent), list(list) +{ +} + +FunctionListModel::~FunctionListModel() {} + +QModelIndex FunctionListModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + return QModelIndex(); + } + + if (row < 0 || row >= list->size()) { + return QModelIndex(); + } + + if (column < 0 || column >= ColumnCount) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex FunctionListModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +int FunctionListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return list->size(); +} + +int FunctionListModel::columnCount(const QModelIndex &) const +{ + return ColumnCount; +} + +QVariant FunctionListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= list->size()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case Name: + return list->at(index.row()).name; + case Offset: + return rzAddressString(list->at(index.row()).offset); + default: + return "unknown"; + } + case Qt::ToolTipRole: + switch (index.column()) { + case Name: + return list->at(index.row()).name; + case Offset: + return rzAddressString(list->at(index.row()).offset); + default: + return "unknown"; + } + default: + return QVariant(); + } +} + +QVariant FunctionListModel::headerData(int section, Qt::Orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case Name: + return "Function Name"; + case Offset: + return "Offset"; + default: + return QVariant(); + } + case Qt::ToolTipRole: + switch (section) { + case Name: + return "Name of the function."; + case Offset: + return "Address of the function."; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +RVA FunctionListModel::address(const QModelIndex &index) const +{ + return list->at(index.row()).offset; +} + +DiffMatchModel::DiffMatchModel(QList *list, QColor cPerf, QColor cPart, + QObject *parent) + : QAbstractListModel(parent), list(list), perfect(cPerf), partial(cPart) +{ +} + +DiffMatchModel::~DiffMatchModel() {} + +int DiffMatchModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return list->size(); +} + +int DiffMatchModel::columnCount(const QModelIndex &) const +{ + return DiffMatchModel::ColumnCount; +} + +QPair DiffMatchModel::address(const QModelIndex &index) const +{ + return QPair(list->at(index.row()).original.offset, + list->at(index.row()).modified.offset); +} + +QVariant DiffMatchModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= list->count()) { + return QVariant(); + } + + const BinDiffMatchDescription &entry = list->at(index.row()); + + switch (role) { + case Qt::DisplayRole: + switch (index.column()) { + case NameOrig: + return entry.original.name; + case SizeOrig: + return QString::asprintf("%llu (%#llx)", entry.original.linearSize, + entry.original.linearSize); + case AddressOrig: + return rzAddressString(entry.original.offset); + case Similarity: + return QString::asprintf("%.2f (%.2f %%)", entry.similarity, entry.similarity * 100.0); + case AddressMod: + return rzAddressString(entry.modified.offset); + case SizeMod: + return QString::asprintf("%llu (%#llx)", entry.modified.linearSize, + entry.modified.linearSize); + case NameMod: + return entry.modified.name; + default: + return QVariant(); + } + + case Qt::ToolTipRole: { + switch (index.column()) { + case NameOrig: + return entry.original.name; + case SizeOrig: + return QString::asprintf("%llu (%#llx)", entry.original.linearSize, + entry.original.linearSize); + case AddressOrig: + return rzAddressString(entry.original.offset); + case Similarity: + return entry.simtype; + case AddressMod: + return rzAddressString(entry.modified.offset); + case SizeMod: + return QString::asprintf("%llu (%#llx)", entry.modified.linearSize, + entry.modified.linearSize); + case NameMod: + return entry.modified.name; + default: + return QVariant(); + } + } + + case Qt::BackgroundRole: { + return gradientByRatio(entry.similarity); + } + + default: + return QVariant(); + } +} + +QVariant DiffMatchModel::headerData(int section, Qt::Orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case NameOrig: + return tr("Name (A)"); + case SizeOrig: + return tr("Size (A)"); + case AddressOrig: + return tr("Address (A)"); + case Similarity: + return tr("Similarity"); + case AddressMod: + return tr("Address (B)"); + case SizeMod: + return tr("Size (B)"); + case NameMod: + return tr("Name (B)"); + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +QColor DiffMatchModel::gradientByRatio(const double ratio) const +{ + const float red = partial.redF() + (ratio * (perfect.redF() - partial.redF())); + const float green = partial.greenF() + (ratio * (perfect.greenF() - partial.greenF())); + const float blue = partial.blueF() + (ratio * (perfect.blueF() - partial.blueF())); + return QColor::fromRgbF(red, green, blue); +} + +DiffMismatchModel::DiffMismatchModel(QList *list, QObject *parent) + : AddressableItemModel(parent), list(list) +{ +} + +DiffMismatchModel::~DiffMismatchModel() {} + +QModelIndex DiffMismatchModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + return QModelIndex(); + } + + if (row < 0 || row >= list->size()) { + return QModelIndex(); + } + + if (column < 0 || column >= ColumnCount) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex DiffMismatchModel::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +int DiffMismatchModel::rowCount(const QModelIndex &) const +{ + return list->count(); +} + +int DiffMismatchModel::columnCount(const QModelIndex &) const +{ + return DiffMismatchModel::ColumnCount; +} + +QVariant DiffMismatchModel::data(const QModelIndex &index, int role) const +{ + if (index.row() >= list->count()) { + return QVariant(); + } + + const FunctionDescription &entry = list->at(index.row()); + + switch (role) { + case Qt::ToolTipRole: + /* fall-thru */ + case Qt::DisplayRole: + switch (index.column()) { + case FuncName: + return entry.name; + case FuncAddress: + return rzAddressString(entry.offset); + case FuncLinearSize: + return QString::asprintf("%llu (%#llx)", entry.linearSize, entry.linearSize); + case FuncNargs: + return QString::asprintf("%llu", entry.nargs); + case FuncNlocals: + return QString::asprintf("%llu", entry.nlocals); + case FuncNbbs: + return QString::asprintf("%llu", entry.nbbs); + case FuncCalltype: + return entry.calltype; + case FuncEdges: + return QString::asprintf("%llu", entry.edges); + case FuncStackframe: + return QString::asprintf("%llu", entry.stackframe); + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +QVariant DiffMismatchModel::headerData(int section, Qt::Orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case FuncName: + return tr("Name"); + case FuncAddress: + return tr("Address"); + case FuncLinearSize: + return tr("Linear Size"); + case FuncNargs: + return tr("Num Args"); + case FuncNlocals: + return tr("Num Locals"); + case FuncNbbs: + return tr("Basic Blocks"); + case FuncCalltype: + return tr("Call Type"); + case FuncEdges: + return tr("Edges"); + case FuncStackframe: + return tr("Stackframe"); + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +RVA DiffMismatchModel::address(const QModelIndex &index) const +{ + return list->at(index.row()).offset; +} + +CutterDiffWindow::CutterDiffWindow(std::unique_ptr bDiff, QWidget *parent) + : QMainWindow(parent), + ui(new Ui::CutterDiffWindow), + bDiff(std::move(bDiff)), + cutterDiff(bDiff->cutterDiff.get()) +{ + ui->setupUi(this); + ui->splitter->setSizes({ 250, 750 }); + ui->splitterHexView->setSizes({ 750, 250 }); + ui->treeViewMatches->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->treeViewMatches, &CutterTreeView::customContextMenuRequested, this, + &CutterDiffWindow::showContextMenuMatches); + connect(ui->treeViewMatches, &CutterTreeView::clicked, this, &CutterDiffWindow::selectFunction); + // connect(bDiff, &BinDiff::complete, this, &CutterDiffWindow::onBinDiffCompleted); + connect(ui->actionDiffNewFiles, &QAction::triggered, this, + &CutterDiffWindow::onActionDiffNewFile); + ui->tabParsing->hide(); + setupFonts(); + showMaximized(); + addLineDiff(); + showDiff(); +} + +CutterDiffWindow::~CutterDiffWindow() +{ + delete ui; +} + +// Work incomplete +void CutterDiffWindow::selectFunction(const QModelIndex &index) {} + +void CutterDiffWindow::showContextMenuMatches(const QPoint &pos) +{ + const QModelIndex index = ui->treeViewMatches->indexAt(pos); + auto addr = matches->address(index); + + QMenu menu(this); + + const QAction *seekTo = menu.addAction("Seek to"); + const QAction *goToAndAlign = menu.addAction("Align HexDiff"); + const QAction *diffFunctionLines = menu.addAction("Line Diff"); + const QAction *copyAddress = menu.addAction("Copy Address"); + + const QAction *selected = menu.exec(ui->treeViewMatches->viewport()->mapToGlobal(pos)); + + if (selected == seekTo) { + if (index.column() < DiffMatchModel::AddressMod) { + seekAndShowHexDiff({ addr.first, RVA_INVALID }); + } else { + seekAndShowHexDiff({ RVA_INVALID, addr.second }); + } + } else if (selected == goToAndAlign) { + seekAndShowHexDiff(addr); + } else if (selected == diffFunctionLines) { + lineDiff->fetchFunctionDisasSplit(addr.first, addr.second); + ui->tabWidget->setCurrentIndex(4); + } else if (selected == copyAddress) { + if (index.column() < DiffMatchModel::AddressMod) { + QApplication::clipboard()->setText(rzAddressString(addr.first)); + } else { + QApplication::clipboard()->setText(rzAddressString(addr.second)); + } + } +} + +void CutterDiffWindow::seekAndShowHexDiff(QPair addr) +{ + if (!hexDiff) { + return; + } + ui->tabWidget->setCurrentIndex(3); + if (addr.first == RVA_INVALID && addr.second == RVA_INVALID) { + return; + } + + if (addr.second == RVA_INVALID) { + hexDiff->seek(addr.first, true); + } else if (addr.first == RVA_INVALID) { + hexDiff->seek(addr.second, false); + } else { + const int transpose = addr.first > addr.second ? -static_cast(addr.first - addr.second) + : static_cast(addr.second - addr.first); + hexDiff->transpose(0, transpose, true); + hexDiff->seek(addr.first); + } +} + +void CutterDiffWindow::setupFonts() {} + +void CutterDiffWindow::showDiff() +{ + if (!bDiff->hasData()) { + return; + } + + const QColor perfect = Config()->getColor( + "gui.match.perfect"); // needs to be added to either cutter or in rizin + const QColor partial = Config()->getColor("gui.match.partial"); + + listMatch = bDiff->matches(); + listDel = bDiff->mismatch(true); + listAdd = bDiff->mismatch(false); + + matches = new DiffMatchModel(&listMatch, perfect, partial, this); + added = new DiffMismatchModel(&listAdd, this); + removed = new DiffMismatchModel(&listDel, this); + + ui->treeViewMatches->setModel(matches); + ui->treeViewMatches->sortByColumn(DiffMatchModel::Similarity, Qt::AscendingOrder); + ui->treeViewMatches->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->treeViewRemoved->setModel(removed); + ui->treeViewRemoved->sortByColumn(DiffMismatchModel::FuncName, Qt::AscendingOrder); + ui->treeViewRemoved->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->treeViewAdded->setModel(added); + ui->treeViewAdded->sortByColumn(DiffMismatchModel::FuncName, Qt::AscendingOrder); + ui->treeViewAdded->setContextMenuPolicy(Qt::CustomContextMenu); + + fcnsA = cutterDiff->getFunctionList(true); + fcnsB = cutterDiff->getFunctionList(false); + modelA = new FunctionListModel(&fcnsA, this); + modelB = new FunctionListModel(&fcnsB, this); + + const QFontMetrics fm(ui->fcnsALabel->font()); + ui->treeViewFcnsA->setModel(modelA); + ui->fcnsALabel->setText( + fm.elidedText(cutterDiff->getFileName(true), Qt::ElideRight, ui->fcnsALabel->width())); + ui->labelInfoA->setText( + fm.elidedText(cutterDiff->getFileName(true), Qt::ElideRight, ui->labelInfoA->width())); + ui->treeViewFcnsB->setModel(modelB); + ui->fcnsBLabel->setText( + fm.elidedText(cutterDiff->getFileName(false), Qt::ElideRight, ui->fcnsBLabel->width())); + ui->labelInfoB->setText( + fm.elidedText(cutterDiff->getFileName(false), Qt::ElideRight, ui->labelInfoB->width())); + addHexDiff(); + connect(ui->treeViewFcnsA, &CutterTreeView::clicked, this, [this](const QModelIndex &index) { + seekAndShowHexDiff({ modelA->address(index), RVA_INVALID }); + }); + connect(ui->treeViewFcnsB, &CutterTreeView::clicked, this, [this](const QModelIndex &index) { + seekAndShowHexDiff({ RVA_INVALID, modelB->address(index) }); + }); + connect(ui->treeViewAdded, &CutterTreeView::doubleClicked, this, + [this](const QModelIndex &index) { + seekAndShowHexDiff({ RVA_INVALID, added->address(index) }); + }); + connect(ui->treeViewRemoved, &CutterTreeView::doubleClicked, this, + [this](const QModelIndex &index) { + seekAndShowHexDiff({ RVA_INVALID, removed->address(index) }); + }); + connect(ui->treeViewMatches, &CutterTreeView::doubleClicked, this, + [this](const QModelIndex &index) { + auto addr = matches->address(index); + if (index.column() < DiffMatchModel::AddressMod) { + seekAndShowHexDiff({ addr.first, RVA_INVALID }); + } else { + seekAndShowHexDiff({ RVA_INVALID, addr.second }); + } + }); + + // Transpose + + connect(ui->shiftUpA, &QPushButton::clicked, this, [this]() { hexDiff->transpose(-1, 0); }); + connect(ui->shiftDownA, &QPushButton::clicked, this, [this]() { hexDiff->transpose(1, 0); }); + connect(ui->shiftUpB, &QPushButton::clicked, this, [this]() { hexDiff->transpose(0, -1); }); + connect(ui->shiftDownB, &QPushButton::clicked, this, [this]() { hexDiff->transpose(0, 1); }); + ui->tabParsing->show(); + + // Parsing + + // Info + + ui->copyMD5A->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copySHA1A->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copySHA256A->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copyCRC32A->setIcon(QIcon(":/img/icons/copy.svg")); + + ui->copyMD5B->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copySHA1B->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copySHA256B->setIcon(QIcon(":/img/icons/copy.svg")); + ui->copyCRC32B->setIcon(QIcon(":/img/icons/copy.svg")); + + // Setup Placeholders + const QString placeholder = tr("Select bytes to display information"); + + ui->bytesMD5A->setPlaceholderText(placeholder); + ui->bytesEntropyA->setPlaceholderText(placeholder); + ui->bytesSHA1A->setPlaceholderText(placeholder); + ui->bytesSHA256A->setPlaceholderText(placeholder); + ui->bytesCRC32A->setPlaceholderText(placeholder); + + ui->bytesMD5B->setPlaceholderText(placeholder); + ui->bytesEntropyB->setPlaceholderText(placeholder); + ui->bytesSHA1B->setPlaceholderText(placeholder); + ui->bytesSHA256B->setPlaceholderText(placeholder); + ui->bytesCRC32B->setPlaceholderText(placeholder); + + // HexDiff signals + connect(hexDiff, &HexDiff::selectionChanged, this, &CutterDiffWindow::selectionChanged); +} + +void CutterDiffWindow::onActionDiffNewFile() +{ + auto loadDiff = new DiffLoadDialog(parentWidget()); + loadDiff->show(); + loadDiff->raise(); +} + +void CutterDiffWindow::addHexDiff() +{ + if (!hexDiff) { + hexDiff = new HexDiff(cutterDiff, this); + } + ui->hexDiffContainer->layout()->addWidget(hexDiff); + const QFont font = Config()->getFont(); + hexDiff->setMonospaceFont(font); +} + +void CutterDiffWindow::addLineDiff() +{ + if (!lineDiff) { + lineDiff = new LineDiffWidget(cutterDiff, this); + } + ui->lineDiffContainer->layout()->addWidget(lineDiff); +} + +void CutterDiffWindow::onCopyMD5AClicked() +{ + const QString md5A = ui->bytesMD5A->text(); + QApplication::clipboard()->setText(md5A); +} + +void CutterDiffWindow::onCopyShA1AClicked() +{ + const QString sha1A = ui->bytesSHA1A->text(); + QApplication::clipboard()->setText(sha1A); +} + +void CutterDiffWindow::onCopyShA256AClicked() +{ + + const QString sha256A = ui->bytesSHA256A->text(); + QApplication::clipboard()->setText(sha256A); +} + +void CutterDiffWindow::onCopyCrC32AClicked() +{ + const QString crc32A = ui->bytesCRC32A->text(); + QApplication::clipboard()->setText(crc32A); +} + +void CutterDiffWindow::onCopyMD5BClicked() +{ + const QString md5B = ui->bytesMD5B->text(); + QApplication::clipboard()->setText(md5B); +} + +void CutterDiffWindow::onCopyShA1BClicked() +{ + const QString sha1B = ui->bytesSHA1B->text(); + QApplication::clipboard()->setText(sha1B); +} + +void CutterDiffWindow::onCopyShA256BClicked() +{ + + const QString sha256B = ui->bytesSHA256B->text(); + QApplication::clipboard()->setText(sha256B); +} + +void CutterDiffWindow::onCopyCrC32BClicked() +{ + const QString crc32B = ui->bytesCRC32B->text(); + QApplication::clipboard()->setText(crc32B); +} + +void CutterDiffWindow::selectionChanged(HexDiff::Selection selection) +{ + if (selection.empty) { + clearParseWindow(); + } else { + updateParseWindow(selection); + } +} + +void CutterDiffWindow::updateParseWindow(HexDiff::Selection selection) +{ + const int size = selection.endAddress - selection.startAddress + 1; + if (ui->tabParsing->currentIndex() == 1) { + RzHashSize digestSize = 0; + const CutterDiffLocked cutterDiff(this->cutterDiff); + const ut64 oldOffsetA = cutterDiff.coreA->offset; + const ut64 oldOffsetB = cutterDiff.coreB->offset; + rz_core_seek(cutterDiff.coreA, selection.startAddress, true); + rz_core_seek(cutterDiff.coreB, selection.startAddressB, true); + const ut8 *blockA = cutterDiff.coreA->block; + const ut8 *blockB = cutterDiff.coreB->block; + char *digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreA->hash, "md5", + blockA, size, &digestSize, false); + ui->bytesMD5A->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreB->hash, "md5", blockB, + size, &digestSize, false); + ui->bytesMD5B->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreA->hash, "sha1", blockA, + size, &digestSize, false); + ui->bytesSHA1A->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreB->hash, "sha1", blockB, + size, &digestSize, false); + ui->bytesSHA1B->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreA->hash, "sha256", blockA, + size, &digestSize, false); + ui->bytesSHA256A->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreB->hash, "sha256", blockB, + size, &digestSize, false); + ui->bytesSHA256B->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreA->hash, "crc32", blockA, + size, &digestSize, false); + ui->bytesCRC32A->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreB->hash, "crc32", blockB, + size, &digestSize, false); + ui->bytesCRC32B->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreA->hash, "entropy", blockA, + size, &digestSize, false); + ui->bytesEntropyA->setText(QString(digest)); + free(digest); + + digest = rz_hash_cfg_calculate_small_block_string(cutterDiff.coreB->hash, "entropy", blockB, + size, &digestSize, false); + ui->bytesEntropyB->setText(QString(digest)); + free(digest); + + rz_core_seek(cutterDiff.coreA, oldOffsetA, true); + rz_core_seek(cutterDiff.coreB, oldOffsetB, true); + ui->bytesMD5A->setCursorPosition(0); + ui->bytesSHA1A->setCursorPosition(0); + ui->bytesSHA256A->setCursorPosition(0); + ui->bytesCRC32A->setCursorPosition(0); + ui->bytesMD5B->setCursorPosition(0); + ui->bytesSHA1B->setCursorPosition(0); + ui->bytesSHA256B->setCursorPosition(0); + ui->bytesCRC32B->setCursorPosition(0); + } +} + +void CutterDiffWindow::clearParseWindow() +{ + ui->bytesEntropyA->setText(""); + ui->bytesMD5A->setText(""); + ui->bytesSHA1A->setText(""); + ui->bytesSHA256A->setText(""); + ui->bytesCRC32A->setText(""); + + ui->bytesEntropyB->setText(""); + ui->bytesMD5B->setText(""); + ui->bytesSHA1B->setText(""); + ui->bytesSHA256B->setText(""); + ui->bytesCRC32B->setText(""); +} diff --git a/src/tools/bindiff/CutterDiffWindow.h b/src/tools/bindiff/CutterDiffWindow.h new file mode 100644 index 0000000000..8cd6674172 --- /dev/null +++ b/src/tools/bindiff/CutterDiffWindow.h @@ -0,0 +1,182 @@ +#ifndef CUTTERDIFFWINDOW_H +#define CUTTERDIFFWINDOW_H + +#include "HexDiff.h" +#include "LineDiffWidget.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace Ui { +class CutterDiffWindow; +} + +class CutterDiffWindow; + +class DiffMatchModel : public QAbstractListModel +{ + Q_OBJECT + + friend CutterDiffWindow; + +public: + enum Column : ut8 { + NameOrig = 0, + SizeOrig, + AddressOrig, + Similarity, + AddressMod, + SizeMod, + NameMod, + ColumnCount + }; + + DiffMatchModel(QList *list, QColor cPerf, QColor cPart, + QObject *parent = nullptr); + ~DiffMatchModel(); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + QColor gradientByRatio(const double ratio) const; + + QPair address(const QModelIndex &index) const; + + void diffNew(const QString &fileA, const QString &fileB, int compareLogic, int analysisLevel); + void clearDiff(); + +private: + QList *list; + + QColor perfect, partial; +}; + +class DiffMismatchModel : public AddressableItemModel<> +{ + Q_OBJECT + + friend CutterDiffWindow; + +public: + enum Column : ut8 { + FuncName = 0, + FuncAddress, + FuncLinearSize, + FuncNargs, + FuncNlocals, + FuncNbbs, + FuncCalltype, + FuncEdges, + FuncStackframe, + ColumnCount + }; + + DiffMismatchModel(QList *list, QObject *parent = nullptr); + ~DiffMismatchModel(); + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + RVA address(const QModelIndex &index) const override; + +private: + QList *list; +}; + +class FunctionListModel : public AddressableItemModel<> +{ + Q_OBJECT +public: + FunctionListModel(QList *list, QObject *parent = nullptr); + ~FunctionListModel(); + enum Column : ut8 { Name, Offset, ColumnCount }; + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + RVA address(const QModelIndex &index) const override; + void refreshModel(); + bool highLightAddress(); + +private: + QList *list; +}; + +class CutterDiffWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit CutterDiffWindow(std::unique_ptr bDiff, QWidget *parent = nullptr); + ~CutterDiffWindow(); + void seekAndShowHexDiff(QPair addr); +public slots: + // void onBinDiffCompleted(); + void onActionDiffNewFile(); +private slots: + void onCopyMD5AClicked(); + void onCopyShA1AClicked(); + void onCopyShA256AClicked(); + void onCopyCrC32AClicked(); + void onCopyMD5BClicked(); + void onCopyShA1BClicked(); + void onCopyShA256BClicked(); + void onCopyCrC32BClicked(); + void selectionChanged(HexDiff::Selection selection); + void showContextMenuMatches(const QPoint &pos); + void selectFunction(const QModelIndex &index); + void showDiff(); + +private: + Ui::CutterDiffWindow *ui; + CutterDiff *cutterDiff; + // BinDiff thread which fetches basic analysis results and processing + std::unique_ptr bDiff; + // model for matched functions of both binaries + DiffMatchModel *matches; + // model for added functions + DiffMismatchModel *added; + // model for removed functions + DiffMismatchModel *removed; + // match results + QList listMatch; + // added function descriptions + QList listDel; + // removed function descriptions + QList listAdd; + // model a functions + FunctionListModel *modelA; + // model b functions + FunctionListModel *modelB; + QList fcnsA; + QList fcnsB; + HexDiff *hexDiff = nullptr; + LineDiffWidget *lineDiff = nullptr; + void addHexDiff(); + void addLineDiff(); + void setupFonts(); + void clearParseWindow(); + void updateParseWindow(HexDiff::Selection selection); +}; + +#endif // CUTTERDIFFWINDOW_H diff --git a/src/tools/bindiff/CutterDiffWindow.ui b/src/tools/bindiff/CutterDiffWindow.ui new file mode 100644 index 0000000000..8faf8e3af6 --- /dev/null +++ b/src/tools/bindiff/CutterDiffWindow.ui @@ -0,0 +1,836 @@ + + + CutterDiffWindow + + + + 0 + 0 + 1080 + 720 + + + + Cutter Diff + + + + + + + + 0 + 0 + + + + + 300 + 0 + + + + + 0 + 0 + + + + Qt::Orientation::Horizontal + + + false + + + + + 0 + 0 + + + + + 100 + 200 + + + + Qt::Orientation::Vertical + + + + + + + Functions A + + + + + + + + + + + + + + + + + + Functions B + + + + + + + + + + + + + + + + + 0 + 0 + + + + 0 + + + + Match Results + + + + + + 0 + + + false + + + false + + + + + + + + Added + + + + + + 0 + + + false + + + false + + + + + + + + Removed + + + + + + 0 + + + false + + + false + + + + + + + + Hex Diff + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Horizontal + + + false + + + + + + + + 0 + + + + + + 0 + 0 + + + + 1 + + + + Diffing + + + + + + Shift + + + + + + + + + + + File A + + + + + + + â–² + + + + + + + â–¼ + + + + + + + + + + + FileB + + + + + + + â–² + + + + + + + â–¼ + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 174 + + + + + + + + + Info + + + + + + INFO A + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 0 + + + 8 + + + 10 + + + + + + 0 + 0 + + + + 4 + + + SHA256: + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + SHA1: + + + + + + + Copy SHA256 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + Copy SHA1 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + Copy CRC32 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + Entropy: + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + MD5: + + + + + + + + 0 + 0 + + + + CRC32: + + + + + + + Copy MD5 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + + INFO B + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + 0 + + + 8 + + + 10 + + + + + + 0 + 0 + + + + 4 + + + SHA256: + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + SHA1: + + + + + + + Copy SHA256 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + Copy SHA1 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + Copy CRC32 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + Entropy: + + + + + + + + 0 + 0 + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + 0 + 0 + + + + MD5: + + + + + + + + 0 + 0 + + + + CRC32: + + + + + + + Copy MD5 + + + Qt::LayoutDirection::RightToLeft + + + + + + + + + + true + + + Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter + + + true + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + LineDiff + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + 0 + 0 + 1080 + 22 + + + + + File + + + + + + + + + Diff New Files + + + + + + CutterTreeView + QTreeView +
widgets/CutterTreeView.h
+ 1 +
+
+ + +
diff --git a/src/tools/bindiff/DiffLoadDialog.cpp b/src/tools/bindiff/DiffLoadDialog.cpp new file mode 100644 index 0000000000..e56377e5b6 --- /dev/null +++ b/src/tools/bindiff/DiffLoadDialog.cpp @@ -0,0 +1,172 @@ +#include "DiffLoadDialog.h" + +#include "ui_DiffLoadDialog.h" + +#include +#include +#include +#include +#include + +#include +#include + +DiffLoadDialog::DiffLoadDialog(QWidget *parent) : QDialog(parent), ui(new Ui::DiffLoadDialog) +{ + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + setModal(true); + + // ui->lineEditFileA->setReadOnly(true); + ui->lineEditFileA->setText(""); + + // ui->lineEditFileB->setReadOnly(true); + ui->lineEditFileB->setText(""); + + ui->comboBoxAnalysis->addItem(tr("Basic")); + ui->comboBoxAnalysis->addItem(tr("Auto")); + ui->comboBoxAnalysis->addItem(tr("Experimental")); + + ui->comboBoxCompare->addItem(tr("Default")); + ui->comboBoxCompare->addItem(tr("All functions")); + ui->comboBoxCompare->addItem(tr("Only Symbols")); + + connect(ui->buttonFileAOpen, &QPushButton::clicked, this, + &DiffLoadDialog::onButtonFileAOpenClicked); + connect(ui->buttonFileBOpen, &QPushButton::clicked, this, + &DiffLoadDialog::onButtonFileBOpenClicked); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &DiffLoadDialog::onButtonBoxAccepted); + connect(ui->setCurrentA, &QCheckBox::checkStateChanged, this, + &DiffLoadDialog::onSetCurrentAChanged); + connect(ui->setCurrentB, &QCheckBox::checkStateChanged, this, + &DiffLoadDialog::onSetCurrentBChanged); + + auto index = ui->comboBoxAnalysis->findData(tr("Auto"), Qt::DisplayRole); + ui->comboBoxAnalysis->setCurrentIndex(index); +} + +DiffLoadDialog::~DiffLoadDialog() {} + +void DiffLoadDialog::onSetCurrentAChanged(Qt::CheckState state) +{ + if (state) { + ui->lineEditFileA->setText(Core()->getConfig("file.path")); + ui->setCurrentB->setDisabled(true); + ui->buttonFileAOpen->setDisabled(true); + } else { + ui->lineEditFileA->setText(""); + ui->setCurrentB->setDisabled(false); + ui->buttonFileAOpen->setDisabled(false); + } +} + +void DiffLoadDialog::onSetCurrentBChanged(Qt::CheckState state) +{ + if (state) { + ui->lineEditFileB->setText(Core()->getConfig("file.path")); + ui->setCurrentA->setDisabled(true); + ui->buttonFileBOpen->setDisabled(true); + } else { + ui->lineEditFileB->setText(""); + ui->setCurrentA->setDisabled(false); + ui->buttonFileBOpen->setDisabled(false); + } +} + +QString DiffLoadDialog::getFileA() const +{ + return ui->lineEditFileA->text(); +} + +QString DiffLoadDialog::getFileB() const +{ + return ui->lineEditFileB->text(); +} + +int DiffLoadDialog::getLevel() const +{ + return ui->comboBoxAnalysis->currentIndex(); +} + +int DiffLoadDialog::getCompare() const +{ + return ui->comboBoxCompare->currentIndex(); +} + +void DiffLoadDialog::onButtonFileAOpenClicked() +{ + QFileDialog dialog(this); + dialog.setWindowTitle(tr("Select File 2")); + dialog.setNameFilters({ tr("All files (*)") }); + + if (!dialog.exec()) { + return; + } + + const QString &fileName = QDir::toNativeSeparators(dialog.selectedFiles().first()); + + if (fileName.isEmpty()) { + return; + } + + const QFileInfo info(fileName); + + if (!info.exists() || !info.isFile()) { + QMessageBox::warning(this, tr("Invalid Path"), + tr("Given file path for File A is not valid.")); + return; + } + + ui->lineEditFileA->setText(fileName); +} + +void DiffLoadDialog::onButtonFileBOpenClicked() +{ + QFileDialog dialog(this); + dialog.setWindowTitle(tr("Select File 2")); + dialog.setNameFilters({ tr("All files (*)") }); + + if (!dialog.exec()) { + return; + } + + const QString &fileName = QDir::toNativeSeparators(dialog.selectedFiles().first()); + + if (fileName.isEmpty()) { + return; + } + + ui->lineEditFileB->setText(fileName); +} + +void DiffLoadDialog::onButtonBoxAccepted() +{ + if (ui->lineEditFileA->text().isEmpty()) { + QMessageBox::warning(this, tr("Empty FileA"), tr("Select a file for diffing.")); + return; + } + const QFileInfo infoA(ui->lineEditFileA->text()); + + if (!infoA.exists() || !infoA.isFile()) { + QMessageBox::warning(this, tr("Invalid Path"), + tr("Given file path for File A is not valid.")); + return; + } + if (ui->lineEditFileB->text().isEmpty()) { + QMessageBox::warning(this, tr("Empty FileB"), tr("Select a file for diffing.")); + return; + } + const QFileInfo infoB(ui->lineEditFileB->text()); + + if (!infoB.exists() || !infoB.isFile()) { + QMessageBox::warning(this, tr("Invalid Path"), + tr("Given file path for File B is not valid.")); + return; + } + auto waitDialog = new DiffWaitDialog(); + waitDialog->show(ui->lineEditFileA->text(), ui->lineEditFileB->text(), + ui->comboBoxAnalysis->currentIndex(), ui->comboBoxCompare->currentIndex()); + waitDialog->setAttribute(Qt::WA_DeleteOnClose); +} + +void DiffLoadDialog::onButtonBoxRejected() {} diff --git a/src/tools/bindiff/DiffLoadDialog.h b/src/tools/bindiff/DiffLoadDialog.h new file mode 100644 index 0000000000..bc33a02d69 --- /dev/null +++ b/src/tools/bindiff/DiffLoadDialog.h @@ -0,0 +1,44 @@ +#ifndef DIFF_LOAD_DIALOG_H +#define DIFF_LOAD_DIALOG_H + +#include "DiffWaitDialog.h" + +#include +#include + +#include +#include + +namespace Ui { +class DiffLoadDialog; +} + +class DiffLoadDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DiffLoadDialog(QWidget *parent = nullptr); + ~DiffLoadDialog(); + + QString getFileA() const; + QString getFileB() const; + int getLevel() const; + int getCompare() const; + +signals: + void startDiffing(); + +private slots: + void onButtonFileAOpenClicked(); + void onButtonFileBOpenClicked(); + void onButtonBoxAccepted(); + void onButtonBoxRejected(); + void onSetCurrentAChanged(Qt::CheckState state); + void onSetCurrentBChanged(Qt::CheckState state); + +private: + std::unique_ptr ui; +}; + +#endif // DIFF_LOAD_DIALOG_H diff --git a/src/tools/bindiff/DiffLoadDialog.ui b/src/tools/bindiff/DiffLoadDialog.ui new file mode 100644 index 0000000000..7a20cb3ba6 --- /dev/null +++ b/src/tools/bindiff/DiffLoadDialog.ui @@ -0,0 +1,176 @@ + + + DiffLoadDialog + + + Qt::WindowModality::NonModal + + + + 0 + 0 + 636 + 226 + + + + + 0 + 0 + + + + Select which file to compare. + + + + QLayout::SizeConstraint::SetMinimumSize + + + + + QLayout::SizeConstraint::SetMinimumSize + + + + + + 0 + 0 + + + + <html><head/><body><p>File to compare to.</p></body></html> + + + File 1(required) + + + + + + + + + + Compare Logic + + + + + + + + + + + + + Analysis Level + + + + + + + Select + + + + + + + Current file + + + + + + + + 0 + 0 + + + + <html><head/><body><p>You can load a previous saved diff instead of performing a new diff scan.</p></body></html> + + + File 2(required) + + + + + + + Select + + + + + + + + + + Current file + + + + + + + + + QLayout::SizeConstraint::SetMinimumSize + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + + + buttonBox + accepted() + DiffLoadDialog + accept() + + + 248 + 234 + + + 157 + 274 + + + + + buttonBox + rejected() + DiffLoadDialog + reject() + + + 316 + 240 + + + 286 + 254 + + + + + diff --git a/src/tools/bindiff/DiffWaitDialog.cpp b/src/tools/bindiff/DiffWaitDialog.cpp new file mode 100644 index 0000000000..9ea5bfcb9a --- /dev/null +++ b/src/tools/bindiff/DiffWaitDialog.cpp @@ -0,0 +1,104 @@ +#include "DiffWaitDialog.h" + +#include "ui_DiffWaitDialog.h" + +#include + +#include +#include + +DiffWaitDialog::DiffWaitDialog(QWidget *parent) + : QDialog(parent), timer(parent), ui(new Ui::DiffWaitDialog) +{ + ui->setupUi(this); + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + setModal(true); + bDiff.reset(new BinDiff()); + + ui->lineEditNFuncs->setReadOnly(true); + ui->lineEditMatches->setReadOnly(true); + ui->lineEditOriginal->setReadOnly(true); + ui->lineEditModified->setReadOnly(true); + ui->progressBar->setValue(0); + ui->lineEditNFuncs->setText("0"); + ui->lineEditMatches->setText("0"); + + const QTime zero(0, 0, 0, 0); + ui->lineEditElapsedTime->setText(zero.toString("hh:mm:ss")); + ui->lineEditEstimatedTime->setText(zero.toString("hh:mm:ss")); +} + +DiffWaitDialog::~DiffWaitDialog() +{ + if (bDiff && bDiff->isRunning()) { + bDiff->cancel(); + bDiff->wait(); + } +} + +void DiffWaitDialog::show(const QString &original, const QString &modified, int level, int compare) +{ + connect(this, &DiffWaitDialog::cancelJob, bDiff.get(), &BinDiff::cancel); + connect(bDiff.get(), &BinDiff::progress, this, &DiffWaitDialog::onProgress); + connect(bDiff.get(), &BinDiff::complete, this, &DiffWaitDialog::onCompletion); + connect(&timer, &QTimer::timeout, this, &DiffWaitDialog::updateElapsedTime); + + ui->lineEditOriginal->setText(original); + ui->lineEditModified->setText(modified); + + bDiff->setAnalysisLevel(level); + bDiff->setCompareLogic(compare); + bDiff->setFileB(modified); + bDiff->setFileA(original); + eTimer.restart(); + timer.setSingleShot(false); + timer.start(1000); + + bDiff->start(); + this->QDialog::show(); +} + +void DiffWaitDialog::onProgress(BinDiffStatusDescription status) +{ + const int partial = status.total - status.nLeft; + const ut32 progress = (100 * partial) / status.total; + ui->progressBar->setValue(progress); + ui->lineEditNFuncs->setText(QString::asprintf("%lu", status.nLeft)); + ui->lineEditMatches->setText(QString::asprintf("%lu", status.nMatch)); + + const double speed = ((double)partial) / ((double)eTimer.elapsed()); + ut64 seconds = (((double)status.nLeft) / speed) / 1000ull; + const int hours = seconds / 3600; + seconds -= (hours * 3600); + const int minutes = seconds / 60; + seconds = seconds % 60; + const QTime estimated(hours, minutes, seconds, 0); + ui->lineEditEstimatedTime->setText(estimated.toString("hh:mm:ss")); +} + +void DiffWaitDialog::onCompletion() +{ + timer.stop(); + auto *diffWindow = new CutterDiffWindow(std::move(bDiff)); + diffWindow->setAttribute(Qt::WA_DeleteOnClose); + diffWindow->show(); + close(); +} + +void DiffWaitDialog::updateElapsedTime() +{ + ut64 seconds = eTimer.elapsed() / 1000ull; + const int hours = seconds / 3600; + seconds -= (hours * 3600); + const int minutes = seconds / 60; + seconds = seconds % 60; + const QTime current(hours, minutes, seconds, 0); + ui->lineEditElapsedTime->setText(current.toString("hh:mm:ss")); +} + +void DiffWaitDialog::onButtonBoxRejected() +{ + timer.stop(); + emit cancelJob(); + close(); +} diff --git a/src/tools/bindiff/DiffWaitDialog.h b/src/tools/bindiff/DiffWaitDialog.h new file mode 100644 index 0000000000..71115e7f9c --- /dev/null +++ b/src/tools/bindiff/DiffWaitDialog.h @@ -0,0 +1,48 @@ +#ifndef DIFF_WAIT_DIALOG_H +#define DIFF_WAIT_DIALOG_H + +#include "CutterDiffWindow.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Ui { +class DiffWaitDialog; +} + +class DiffWaitDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DiffWaitDialog(QWidget *parent = nullptr); + ~DiffWaitDialog(); + + void show(const QString &original, const QString &modified, int level, int compare); + +public slots: + void onProgress(BinDiffStatusDescription status); + void onCompletion(); + void updateElapsedTime(); + +signals: + void cancelJob(); + +private slots: + void onButtonBoxRejected(); + +private: + QElapsedTimer eTimer; + std::unique_ptr bDiff; + QTimer timer; + std::unique_ptr ui; +}; + +#endif // DIFF_WAIT_DIALOG_H diff --git a/src/tools/bindiff/DiffWaitDialog.ui b/src/tools/bindiff/DiffWaitDialog.ui new file mode 100644 index 0000000000..8b2b89d712 --- /dev/null +++ b/src/tools/bindiff/DiffWaitDialog.ui @@ -0,0 +1,151 @@ + + + DiffWaitDialog + + + Qt::WindowModality::NonModal + + + + 0 + 0 + 524 + 342 + + + + + 0 + 0 + + + + Performing binary diffing + + + + QLayout::SizeConstraint::SetMinimumSize + + + + + QLayout::SizeConstraint::SetMinimumSize + + + + + 24 + + + + + + + + + + + + + + + Elapsed Time + + + + + + + Modified File + + + + + + + Matches + + + + + + + + + + Original File + + + + + + + + + + Functions left to compare + + + + + + + + + + Estimated Time + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel + + + + + + + + + Remove item + + + + + Remove all + + + Remove all + + + + + + + buttonBox + rejected() + DiffWaitDialog + reject() + + + 316 + 240 + + + 286 + 254 + + + + + diff --git a/src/tools/bindiff/HexDiff.cpp b/src/tools/bindiff/HexDiff.cpp new file mode 100644 index 0000000000..7879548f97 --- /dev/null +++ b/src/tools/bindiff/HexDiff.cpp @@ -0,0 +1,1829 @@ +#include "HexDiff.h" + +#include "Configuration.h" +#include "Cutter.h" +#include "dialogs/CommentsDialog.h" +#include "dialogs/FlagDialog.h" +#include "dialogs/MarkDialog.h" +#include "dialogs/WriteCommandsDialogs.h" +#include "shortcuts/ShortcutManager.h" +#include "widgets/AddressRangeScrollBar.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr uint64_t maxCopySize = 128 * 1024 * 1024; +constexpr int maxLineWidthPreset = 32; +constexpr int maxLineWidthBytes = 128 * 1024; +constexpr int warningTimeMs = 500; +} + +HexDiff::HexDiff(CutterDiff *cutterDiff, QWidget *parent) + : QScrollArea(parent), + cursorEnabled(true), + cursorArea(DiffArea::ItemA), + updatingSelection(false), + itemByteLen(1), + itemGroupSize(1), + rowSizeBytes(16), + columnMode(ColumnMode::PowerOf2), + itemFormat(ItemFormatHex), + itemBigEndian(false), + addrCharLen(AddrWidth64), + showHeader(true), + showAscii(true), + showExHex(true), + showExAddr(true), + cutterDiff(cutterDiff), + vScrollBar(new AddressRangeScrollBar(this)) +{ + setMouseTracking(true); + setFocusPolicy(Qt::FocusPolicy::StrongFocus); + connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &HexDiff::updateViewport); + + connect(Config(), &Configuration::colorsUpdated, this, &HexDiff::updateColors); + connect(Config(), &Configuration::fontsUpdated, this, + [this]() { setMonospaceFont(Config()->getFont()); }); + + setVerticalScrollBar(vScrollBar); + vScrollBar->setPageStep(10); + vScrollBar->setSingleStep(1); + connect(vScrollBar, &AddressRangeScrollBar::scrolled, this, + [this](int lines) { scrollLines(lines, true); }); + connect(vScrollBar, &QScrollBar::valueChanged, this, + [this](int) { setStartAddress(vScrollBar->address()); }); + connect(vScrollBar, &AddressRangeScrollBar::hideScrollBar, this, + [this]() { setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); }); + connect(vScrollBar, &AddressRangeScrollBar::showScrollBar, this, + [this]() { setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); }); + vScrollBar->refreshRange(); + + auto sizeActionGroup = new QActionGroup(this); + for (int i = 1; i <= 8; i *= 2) { + auto *action = new QAction(QString::number(i), this); + action->setCheckable(true); + action->setActionGroup(sizeActionGroup); + connect(action, &QAction::triggered, this, [=, this]() { setItemSize(i); }); + actionsItemSize.append(action); + } + actionsItemSize.at(0)->setChecked(true); + + /* Follow the order in ItemFormat enum */ + QStringList names; + names << tr("Hexadecimal"); + names << tr("Octal"); + names << tr("Decimal"); + names << tr("Signed decimal"); + names << tr("Float"); + + auto formatActionGroup = new QActionGroup(this); + for (int i = 0; i < names.length(); ++i) { + auto *action = new QAction(names.at(i), this); + action->setCheckable(true); + action->setActionGroup(formatActionGroup); + connect(action, &QAction::triggered, this, + [=, this]() { setItemFormat(static_cast(i)); }); + actionsItemFormat.append(action); + } + actionsItemFormat.at(0)->setChecked(true); + actionsItemFormat.at(ItemFormatFloat)->setEnabled(false); + + rowSizeMenu = new QMenu(tr("Bytes per row"), this); + auto columnsActionGroup = new QActionGroup(this); + for (int i = 1; i <= maxLineWidthPreset; i *= 2) { + auto *action = new QAction(QString::number(i), rowSizeMenu); + action->setCheckable(true); + action->setActionGroup(columnsActionGroup); + connect(action, &QAction::triggered, this, [=, this]() { setFixedLineSize(i); }); + rowSizeMenu->addAction(action); + } + rowSizeMenu->addSeparator(); + actionRowSizePowerOf2 = new QAction(tr("Power of 2"), this); + actionRowSizePowerOf2->setCheckable(true); + actionRowSizePowerOf2->setActionGroup(columnsActionGroup); + connect(actionRowSizePowerOf2, &QAction::triggered, this, + [=, this]() { setColumnMode(ColumnMode::PowerOf2); }); + rowSizeMenu->addAction(actionRowSizePowerOf2); + + actionItemBigEndian = new QAction(tr("Big Endian"), this); + actionItemBigEndian->setCheckable(true); + actionItemBigEndian->setEnabled(false); + connect(actionItemBigEndian, &QAction::triggered, this, &HexDiff::setItemEndianness); + + actionHexPairs = new QAction(tr("Bytes as pairs"), this); + actionHexPairs->setCheckable(true); + connect(actionHexPairs, &QAction::triggered, this, &HexDiff::onHexPairsModeEnabled); + + actionCopy = Shortcuts()->makeAction("Hex.copy", this); + addAction(actionCopy); + actionCopy->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut); + connect(actionCopy, &QAction::triggered, this, &HexDiff::copy); + + actionCopyAddress = Shortcuts()->makeAction("General.copyAddress", this); + actionCopyAddress->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut); + connect(actionCopyAddress, &QAction::triggered, this, &HexDiff::copyAddress); + addAction(actionCopyAddress); + + actionSelectRange = new QAction(tr("Select range"), this); + connect(actionSelectRange, &QAction::triggered, this, + [this]() { rangeDialog.openAt(cursor.address); }); + addAction(actionSelectRange); + connect(&rangeDialog, &QDialog::accepted, this, &HexDiff::onRangeDialogAccepted); + + connect(this, &HexDiff::selectionChanged, this, + [this](Selection newSelection) { actionCopy->setEnabled(!newSelection.empty); }); + + updateMetrics(); + updateItemLength(); + + startAddress = 0ULL; + cursor.address = 0ULL; + ctxA.data.reset(new MemoryDiffData(cutterDiff, true)); + ctxB.data.reset(new MemoryDiffData(cutterDiff, false)); + ctxA.file = DiffFile::A; + ctxB.file = DiffFile::B; + + fetchData(); + updateCursorMeta(); + + connect(&cursor.blinkTimer, &QTimer::timeout, this, &HexDiff::onCursorBlinked); + cursor.setBlinkPeriod(1000); + cursor.startBlinking(); + + updateColors(); +} + +void HexDiff::setMonospaceFont(const QFont &font) +{ + if (!(font.styleHint() & QFont::Monospace)) { + /* FIXME: Use default monospace font + setFont(XXX); */ + } + QScrollArea::setFont(font); + monospaceFont = font.resolve(this->font()); + updateMetrics(); + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::setItemSize(int nbytes) +{ + static const QVector values({ 1, 2, 4, 8 }); + + if (!values.contains(nbytes)) { + return; + } + + itemByteLen = nbytes; + if (itemByteLen > rowSizeBytes) { + rowSizeBytes = itemByteLen; + } + + actionsItemFormat.at(ItemFormatFloat)->setEnabled(nbytes >= 4); + actionItemBigEndian->setEnabled(nbytes != 1); + + updateItemLength(); + if (cursorArea > 1 && cursor.address % itemByteLen) { + moveCursor(-int(cursor.address % itemByteLen)); + } + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::setItemFormat(ItemFormat format) +{ + + itemFormat = format; + + bool sizeEnabled = true; + if (format == ItemFormatFloat) { + sizeEnabled = false; + } + actionsItemSize.at(0)->setEnabled(sizeEnabled); + actionsItemSize.at(1)->setEnabled(sizeEnabled); + + updateItemLength(); + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::setItemGroupSize(int size) +{ + itemGroupSize = size; + + updateCounts(); + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::updateCounts() +{ + actionHexPairs->setEnabled(rowSizeBytes > 1 && itemByteLen == 1 + && itemFormat == ItemFormat::ItemFormatHex); + actionHexPairs->setChecked(cutterDiff->getCommonConfigb("hex.pairs")); + if (actionHexPairs->isChecked() && actionHexPairs->isEnabled()) { + itemGroupSize = 2; + } else { + itemGroupSize = 1; + } + + if (columnMode == ColumnMode::PowerOf2) { + int lastGoodSize = itemGroupByteLen(); + for (int i = itemGroupByteLen(); i <= maxLineWidthBytes; i *= 2) { + rowSizeBytes = i; + itemColumns = rowSizeBytes / itemGroupByteLen(); + updateAreasPosition(); + if (horizontalScrollBar()->maximum() == 0) { + lastGoodSize = rowSizeBytes; + } else { + break; + } + } + rowSizeBytes = lastGoodSize; + } + + itemColumns = rowSizeBytes / itemGroupByteLen(); + + // ensure correct action is selected when changing line size programmatically + if (columnMode == ColumnMode::Fixed) { + int w = 1; + const auto &actions = rowSizeMenu->actions(); + for (auto action : actions) { + action->setChecked(false); + } + for (auto action : actions) { + if (w > maxLineWidthPreset) { + break; + } + if (rowSizeBytes == w) { + action->setChecked(true); + } + w *= 2; + } + } else if (columnMode == ColumnMode::PowerOf2) { + actionRowSizePowerOf2->setChecked(true); + } + + updateAreasPosition(); +} + +void HexDiff::setFixedLineSize(int lineSize) +{ + if (lineSize < 1 || lineSize < itemGroupByteLen() || lineSize % itemGroupByteLen()) { + updateCounts(); + return; + } + rowSizeBytes = lineSize; + columnMode = ColumnMode::Fixed; + + updateCounts(); + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::setColumnMode(ColumnMode mode) +{ + columnMode = mode; + + updateCounts(); + fetchData(); + updateCursorMeta(); + + updateViewport(); +} + +void HexDiff::selectRange(RVA start, RVA end) +{ + BasicDiffCursor endCursor(end); + endCursor += 1; + setCursorAddr(endCursor); + selection.set(start, end); + cursorEnabled = false; + emit selectionChanged(getSelection()); +} + +void HexDiff::clearSelection() +{ + setCursorAddr(BasicDiffCursor(cursor.address), false); + emit selectionChanged(getSelection()); +} + +HexDiff::Selection HexDiff::getSelection() +{ + return Selection { selection.isEmpty(), selection.start(), selection.end(), + getAddressB(selection.start()), getAddressB(selection.end()) }; +} + +void HexDiff::seek(uint64_t address, bool orig) +{ + if (!orig) { + if (relTranspose >= 0) { + address -= static_cast(relTranspose); + } else { + address += static_cast(-relTranspose); + } + } + if (cursorArea > 1) { + // when other widget causes seek to the middle of word + // switch to ascii column which operates with byte positions + auto viewOffset = startAddress % itemByteLen; + auto addrOffset = address % itemByteLen; + if ((addrOffset + itemByteLen - viewOffset) % itemByteLen) { + setCursorOnAscii(true); + } + } + setCursorAddr(BasicDiffCursor(address)); +} + +void HexDiff::refresh() +{ + fetchData(); + updateViewport(); +} + +void HexDiff::setItemEndianness(bool bigEndian) +{ + itemBigEndian = bigEndian; + + updateCursorMeta(); // Update cached item character + + updateViewport(); +} + +void HexDiff::updateColors() +{ + borderColor = Config()->getColor("gui.border"); + backgroundColor = Config()->getColor("gui.background"); + b0x00Color = Config()->getColor("b0x00"); + b0x7fColor = Config()->getColor("b0x7f"); + b0xffColor = Config()->getColor("b0xff"); + printableColor = Config()->getColor("ai.write"); + defColor = Config()->getColor("btext"); + addrColor = Config()->getColor("func_var_addr"); + diffColor = Config()->getColor("diff.unmatch"); + + updateCursorMeta(); + updateViewport(); +} + +void HexDiff::paintEvent(QPaintEvent *event) +{ + QPainter painter(viewport()); + painter.setFont(monospaceFont); + + const int xOffset = horizontalScrollBar()->value(); + if (xOffset > 0) { + painter.translate(QPoint(-xOffset, 0)); + } + + const QRect cursorRectA = cursor.screenPos.toAlignedRect(); + const QRect cursorRectB = cursor.screenPosB.toAlignedRect(); + if (event->rect() == cursorRectA || event->rect() == cursorRectB) { + /*cursor blink*/ + drawCursor(painter); + return; + } + + painter.fillRect(event->rect().translated(xOffset, 0), backgroundColor); + + drawHeader(painter, ctxA); + drawHeader(painter, ctxB); + + drawAddrArea(painter, ctxA); + drawItemArea(painter, ctxA); + drawAsciiArea(painter, ctxA); + + drawItemArea(painter, ctxB); + drawAsciiArea(painter, ctxB); + drawAddrArea(painter, ctxB); + + if (!cursorEnabled) { + return; + } + + drawCursor(painter, true); +} + +void HexDiff::updateWidth() +{ + int max = (showAscii ? ctxB.asciiArea.right() : ctxB.itemArea.right()) - viewport()->width(); + if (max < 0) { + max = 0; + } else { + max += charWidth; + } + horizontalScrollBar()->setMaximum(max); + horizontalScrollBar()->setSingleStep(charWidth); +} + +bool HexDiff::isFixedWidth() const +{ + return itemFormat == ItemFormatHex || itemFormat == ItemFormatOct; +} + +void HexDiff::resizeEvent(QResizeEvent *event) +{ + const int oldByteCount = bytesPerScreen(); + updateCounts(); + + if (event->oldSize().height() == event->size().height() && oldByteCount == bytesPerScreen()) { + return; + } + + updateAreasHeight(); + fetchData(); // rowCount was changed + updateCursorMeta(); + + updateViewport(); +} + +DiffFile HexDiff::getFileFromPos(QPoint &pos) const +{ + if (ctxA.asciiArea.contains(pos) || ctxA.itemArea.contains(pos)) { + return DiffFile::A; + } + return DiffFile::B; +} + +void HexDiff::mouseMoveEvent(QMouseEvent *event) +{ + QPoint pos = event->pos(); + pos.rx() += horizontalScrollBar()->value(); + + const bool cursorOnArea = ctxA.asciiArea.contains(pos) || ctxB.asciiArea.contains(pos) + || ctxA.itemArea.contains(pos) || ctxB.itemArea.contains(pos); + + auto mouseAddr = mousePosToAddrA(pos).address; + + QString infoText; + if (!updatingSelection && (cursorOnArea)) { + QString metaData = getFileFromPos(pos) == DiffFile::A + ? getFlagsAndComment(mouseAddr, ctxA) + : getFlagsAndComment(getAddressB(mouseAddr), ctxB); // has to be redefined with ctx + if (!metaData.isEmpty() && (ctxA.itemArea.contains(pos) || ctxB.itemArea.contains(pos))) { + infoText = metaData.replace(",", ", "); + } + + if (!infoText.isEmpty()) { + // forces tooltip to follow cursor movement + QToolTip::showText(mapToGlobal(event->pos()), infoText + " ", this); + + QToolTip::showText(mapToGlobal(event->pos()), infoText, this); + } else { + QToolTip::hideText(); + } + } else { + QToolTip::hideText(); + } + + if (!updatingSelection) { + if (cursorOnArea) { + setCursor(Qt::IBeamCursor); + } else { + setCursor(Qt::ArrowCursor); + } + return; + } + + auto &area = currentArea(); + if (pos.x() < area.left()) { + pos.setX(area.left()); + } else if (pos.x() > area.right()) { + pos.setX(area.right()); + } + auto addr = currentAreaPosToAddrA(pos, true); + setCursorAddr(addr, true); + + /* Stop blinking */ + cursorEnabled = false; + + updateViewport(); +} + +const QRectF &HexDiff::currentArea() +{ + switch (cursorArea) { + case DiffArea::AsciiA: + return ctxA.asciiArea; + case DiffArea::AsciiB: + return ctxB.asciiArea; + case DiffArea::ItemA: + return ctxA.itemArea; + case DiffArea::ItemB: + return ctxB.itemArea; + } + return ctxA.itemArea; +} + +DiffArea HexDiff::posToDiffArea(QPoint &point) const +{ + if (ctxB.itemArea.contains(point)) { + return DiffArea::ItemB; + } else if (ctxA.asciiArea.contains(point)) { + return DiffArea::AsciiA; + } else if (ctxB.asciiArea.contains(point)) { + return DiffArea::AsciiB; + } + return DiffArea::ItemA; +} + +bool HexDiff::diffItemsAt(uint64_t addrA) const +{ + quint8 a[8]; + quint8 b[8]; + ctxA.data->copy(a, addrA, static_cast(itemByteLen)); + ctxB.data->copy(b, getAddressB(addrA), static_cast(itemByteLen)); + return memcmp(a, b, itemByteLen); +} + +void HexDiff::mousePressEvent(QMouseEvent *event) +{ + QPoint pos(event->pos()); + pos.rx() += horizontalScrollBar()->value(); + + if (event->button() == Qt::LeftButton) { + const DiffArea area = posToDiffArea(pos); + const bool selectingData = area > DiffArea::AsciiB; + const bool selecting = selectingData || area <= DiffArea::AsciiB; + const bool holdingShift = event->modifiers() == Qt::ShiftModifier; + + if (selecting) { + updatingSelection = true; + setCursorOnArea(area); + auto cursorPosition = currentAreaPosToAddrA(pos, true); + setCursorAddr(cursorPosition, holdingShift); + updateViewport(); + } + } +} + +void HexDiff::mouseDoubleClickEvent(QMouseEvent *event) +{ + QPoint pos(event->pos()); + pos.rx() += horizontalScrollBar()->value(); +} + +void HexDiff::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (selection.isEmpty()) { + selection.init(BasicDiffCursor(cursor.address)); + cursorEnabled = true; + updateViewport(); + } + updatingSelection = false; + } +} + +void HexDiff::wheelEvent(QWheelEvent *event) +{ + + // according to Qt doc 1 row per 5 degrees, angle measured in 1/8 of degree + const int dy = event->angleDelta().y() / (8 * 5); + scrollLines(dy); + vScrollBar->showTransientScrollBar(); +} + +bool HexDiff::event(QEvent *event) +{ + // prefer treating keys like 's' 'g' '.' as typing input instead of global shortcuts + if (event->type() == QEvent::ShortcutOverride) { + auto keyEvent = static_cast(event); + auto modifiers = keyEvent->modifiers(); + if ((modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier + || modifiers == Qt::KeypadModifier) + && keyEvent->key() < Qt::Key_Escape) { + keyEvent->accept(); + return true; + } + } + + return QScrollArea::event(event); +} + +void HexDiff::keyPressEvent(QKeyEvent *event) +{ + + bool select = false; + auto moveOrSelect = [event, &select](QKeySequence::StandardKey moveSeq, + QKeySequence::StandardKey selectSeq) -> bool { + if (event->matches(moveSeq)) { + select = false; + return true; + } else if (event->matches(selectSeq)) { + select = true; + return true; + } + return false; + }; + + if (moveOrSelect(QKeySequence::MoveToNextPage, QKeySequence::SelectNextPage)) { + moveCursor(bytesPerScreen(), select); + } else if (moveOrSelect(QKeySequence::MoveToPreviousPage, QKeySequence::SelectPreviousPage)) { + moveCursor(-bytesPerScreen(), select); + } else if (moveOrSelect(QKeySequence::MoveToStartOfLine, QKeySequence::SelectStartOfLine)) { + const int linePos = + int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen())); + moveCursor(-linePos, select); + } else if (moveOrSelect(QKeySequence::MoveToEndOfLine, QKeySequence::SelectEndOfLine)) { + const int linePos = + int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen())); + moveCursor(itemRowByteLen() - linePos, select); + } + + if (moveOrSelect(QKeySequence::MoveToNextLine, QKeySequence::SelectNextLine)) { + moveCursor(itemRowByteLen(), select, OverflowMove::Ignore); + } else if (moveOrSelect(QKeySequence::MoveToPreviousLine, QKeySequence::SelectPreviousLine)) { + moveCursor(-itemRowByteLen(), select, OverflowMove::Ignore); + } else if (moveOrSelect(QKeySequence::MoveToNextChar, QKeySequence::SelectNextChar) + || moveOrSelect(QKeySequence::MoveToNextWord, QKeySequence::SelectNextWord)) { + moveCursor(cursorArea < DiffArea::ItemA ? 1 : itemByteLen, select); + } else if (moveOrSelect(QKeySequence::MoveToPreviousChar, QKeySequence::SelectPreviousChar) + || moveOrSelect(QKeySequence::MoveToPreviousWord, + QKeySequence::SelectPreviousWord)) { + moveCursor(cursorArea < DiffArea::ItemA ? -1 : -itemByteLen, select); + } + + if (cursorArea > DiffArea::AsciiB) { + if (moveOrSelect(QKeySequence::MoveToNextChar, QKeySequence::SelectNextChar)) { + if (select) { + moveCursor(itemByteLen, select); + } else { + if (!selection.isEmpty()) { + clearSelection(); + } + } + updateViewport(); + } else if (event->matches(QKeySequence::SelectPreviousChar)) { + moveCursor(-itemByteLen, true); + } else if (moveOrSelect(QKeySequence::MoveToNextWord, QKeySequence::SelectNextWord)) { + moveCursor(itemByteLen, select); + } else if (event->matches(QKeySequence::MoveToPreviousWord)) { + moveCursor(-itemByteLen, false); + } else if (event->matches(QKeySequence::SelectPreviousWord)) { + moveCursor(-itemByteLen, true); + } + } +} + +void HexDiff::contextMenuEvent(QContextMenuEvent *event) +{ + const QPoint pt = event->pos(); + bool mouseOutsideSelection = false; + if (event->reason() == QContextMenuEvent::Mouse) { + auto mouseAddr = mousePosToAddrA(pt).address; + if (ctxA.asciiArea.contains(pt)) { + cursorArea = DiffArea::AsciiA; + } else if (ctxB.asciiArea.contains(pt)) { + cursorArea = DiffArea::AsciiB; + } else if (ctxB.itemArea.contains(pt)) { + cursorArea = DiffArea::ItemB; + } else { + cursorArea = DiffArea::ItemA; + } + if (selection.isEmpty()) { + seek(mouseAddr); + } else { + mouseOutsideSelection = !selection.contains(mouseAddr); + } + } + + auto disableOutsideSelectionActions = [this](bool disable) { + actionCopyAddress->setDisabled(disable); + }; + + auto *menu = new QMenu(this); + QMenu *sizeMenu = menu->addMenu(tr("Item size:")); + sizeMenu->addActions(actionsItemSize); + QMenu *formatMenu = menu->addMenu(tr("Item format:")); + formatMenu->addActions(actionsItemFormat); + menu->addMenu(rowSizeMenu); + menu->addAction(actionHexPairs); + menu->addAction(actionItemBigEndian); + menu->addSeparator(); + menu->addAction(actionCopy); + disableOutsideSelectionActions(mouseOutsideSelection); + menu->addAction(actionCopyAddress); + menu->addActions(this->actions()); + + menu->exec(mapToGlobal(pt)); + disableOutsideSelectionActions(false); + menu->deleteLater(); +} + +void HexDiff::onCursorBlinked() +{ + if (!cursorEnabled) { + return; + } + cursor.blink(); + const int xOffset = horizontalScrollBar()->value(); + viewport()->update(cursor.screenPos.toAlignedRect().translated(-xOffset, 0)); + viewport()->update(cursor.screenPosB.toAlignedRect().translated(-xOffset, 0)); +} + +void HexDiff::onHexPairsModeEnabled(bool enable) +{ + // Sync configuration + cutterDiff->setCommonConfigb("hex.pairs", enable); + if (enable) { + setItemGroupSize(2); + } else { + setItemGroupSize(1); + } +} + +void HexDiff::copy() +{ + if (selection.isEmpty() || selection.size() > maxCopySize) { + return; + } + QString x; + if (cursorArea < 2) { + if (cursorArea == DiffArea::AsciiA) { + x = QString::fromUtf8( + cutterDiff->ioRead(selection.start(), (int)selection.size(), true)); + } else { + x = QString::fromUtf8(cutterDiff->ioRead(getAddressB(selection.start()), + (int)selection.size(), false)); + } + } else { + if (cursorArea == DiffArea::ItemA) { + x = cutterDiff->ioRead(selection.start(), (int)selection.size(), true).toHex(); + } else { + x = cutterDiff->ioRead(getAddressB(selection.start()), (int)selection.size(), false) + .toHex(); + } + } + QApplication::clipboard()->setText(x); +} + +void HexDiff::copyAddress() +{ + uint64_t addr = getLocationAddress(); + if (cursorArea % 2) { + addr = getAddressB(addr); + } + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(rzAddressString(addr)); +} + +void HexDiff::onRangeDialogAccepted() +{ + if (rangeDialog.empty()) { + seek(rangeDialog.getStartAddress()); + return; + } + selectRange(rangeDialog.getStartAddress(), rangeDialog.getEndAddress()); +} + +void HexDiff::updateItemLength() +{ + itemPrefixLen = 0; + itemPrefix.clear(); + + switch (itemFormat) { + case ItemFormatHex: + itemCharLen = 2 * itemByteLen; + if (itemByteLen > 1 && showExHex) { + itemPrefixLen = hexPrefix.length(); + itemPrefix = hexPrefix; + } + break; + case ItemFormatOct: + itemCharLen = (itemByteLen * 8 + 3) / 3; + break; + case ItemFormatDec: + switch (itemByteLen) { + case 1: + itemCharLen = 3; + break; + case 2: + itemCharLen = 5; + break; + case 4: + itemCharLen = 10; + break; + case 8: + itemCharLen = 20; + break; + } + break; + case ItemFormatSignedDec: + switch (itemByteLen) { + case 1: + itemCharLen = 4; + break; + case 2: + itemCharLen = 6; + break; + case 4: + itemCharLen = 11; + break; + case 8: + itemCharLen = 20; + break; + } + break; + case ItemFormatFloat: + if (itemByteLen < 4) { + itemByteLen = 4; + } + // FIXME + itemCharLen = 3 * itemByteLen; + break; + } + + itemCharLen += itemPrefixLen; + + updateCounts(); +} + +void HexDiff::drawHeader(QPainter &painter, DiffFileContext &ctx) +{ + if (!showHeader) { + return; + } + + int offset = 0; + QRectF rect(ctx.itemArea.left(), 0, itemWidth(), lineHeight); + + painter.setPen(addrColor); + + for (int j = 0; j < itemColumns; ++j) { + for (int k = 0; k < itemGroupSize; ++k, offset += itemByteLen) { + painter.drawText(rect, Qt::AlignVCenter | Qt::AlignRight, + QString::number(offset, 16).toUpper()); + rect.translate(itemWidth(), 0); + } + rect.translate(columnSpacingWidth(), 0); + } + + rect.moveLeft(ctx.asciiArea.left()); + rect.setWidth(charWidth); + for (int j = 0; j < itemRowByteLen(); ++j) { + painter.drawText(rect, Qt::AlignVCenter | Qt::AlignRight, + QString::number(j % 16, 16).toUpper()); + rect.translate(charWidth, 0); + } +} + +void HexDiff::drawCursor(QPainter &painter, bool shadow) +{ + if (shadow) { + QPen pen(Qt::gray); + pen.setStyle(Qt::DashLine); + painter.setPen(pen); + qreal shadowWidth = charWidth; + if (cursorArea < 2) { + shadowWidth = itemWidth(); + } + shadowCursor.screenPos.setWidth(shadowWidth); + shadowCursor.screenPosB.setWidth(shadowWidth); + painter.drawRect(shadowCursor.screenPos); + painter.drawRect(shadowCursor.screenPosB); + painter.setPen(Qt::SolidLine); + } + + painter.setPen(cursor.cachedColor); + QRectF charRect(cursor.screenPos); + charRect.setWidth(charWidth); + painter.fillRect(charRect, backgroundColor); + painter.drawText(charRect, Qt::AlignVCenter, cursor.cachedChar); + painter.setPen(cursor.cachedColorB); + QRectF charRectB(cursor.screenPosB); + charRectB.setWidth(charWidth); + painter.fillRect(charRectB, backgroundColor); + painter.drawText(charRectB, Qt::AlignVCenter, cursor.cachedCharB); + if (cursor.isVisible) { + painter.setCompositionMode(QPainter::RasterOp_SourceXorDestination); + painter.fillRect(cursor.screenPos, QColor(0xff, 0xff, 0xff)); + painter.fillRect(cursor.screenPosB, QColor(0xff, 0xff, 0xff)); + } +} + +void HexDiff::drawAddrArea(QPainter &painter, DiffFileContext &ctx) +{ + + uint64_t offset = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + + QString addrString; + const QSizeF areaSize((addrCharLen + (showExAddr ? 2 : 0)) * charWidth, lineHeight); + QRectF strRect(ctx.addrArea.topLeft(), areaSize); + + painter.setPen(addrColor); + for (int line = 0; line < visibleLines && offset <= ctx.data->maxIndex(); + ++line, strRect.translate(0, lineHeight), offset += itemRowByteLen()) { + addrString = QString("%1").arg(offset, addrCharLen, 16, QLatin1Char('0')); + if (showExAddr) { + addrString.prepend(hexPrefix); + } + painter.drawText(strRect, Qt::AlignVCenter, addrString); + } + + painter.setPen(borderColor); + + const qreal vLineOffset = ctx.itemArea.left() - charWidth; + painter.drawLine(QLineF(vLineOffset, 0, vLineOffset, viewport()->height())); +} + +uint64_t HexDiff::ctxAddr(uint64_t addrA, DiffFileContext &ctx) +{ + if (ctx.file == DiffFile::A) { + return getAddressB(addrA); + } + return addrA; +} + +HexDiffSelection HexDiff::ctxSelection(DiffFileContext &ctx) +{ + if (ctx.file == DiffFile::A) { + return selection; + } + HexDiffSelection selectionB; + if (!selection.isEmpty()) { + selectionB.set(getAddressB(selection.start()), getAddressB(selection.end())); + } + return selectionB; +} + +void HexDiff::drawItemArea(QPainter &painter, DiffFileContext &ctx) +{ + const uint64_t addr = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + + QRectF itemRect(ctx.itemArea.topLeft(), QSizeF(itemWidth(), lineHeight)); + QColor itemColor; + QString itemString; + + fillSelectionBackground(painter, ctx); + + const HexDiffSelection &ctxSel = ctxSelection(ctx); + + uint64_t itemAddr = addr; + auto &itemCursor = cursorArea < 2 ? shadowCursor : cursor; + for (int line = 0; line < visibleLines; ++line) { + itemRect.moveLeft(ctx.itemArea.left()); + for (int j = 0; j < itemColumns; ++j) { + for (int k = 0; k < itemGroupSize && itemAddr <= ctx.data->maxIndex(); + ++k, itemAddr += itemByteLen) { + + itemString = renderItem(itemAddr - addr, ctx, &itemColor); + + if (!getFlagsAndComment(itemAddr, ctx).isEmpty()) { + QColor markerColor(borderColor); + markerColor.setAlphaF(0.5); + painter.setPen(markerColor); + for (const auto &shape : rangePolygons(itemAddr, itemAddr, false, ctx)) { + painter.drawPolyline(shape); + } + } + if (ctxSel.contains(itemAddr) && cursorArea > 1) { + itemColor = palette().highlightedText().color(); + } + + if (ctx.file == DiffFile::A && diffItemsAt(itemAddr)) { + itemColor = diffColor; + } + + if (ctx.file == DiffFile::B && diffItemsAt(getAddressA(itemAddr))) { + itemColor = diffColor; + } + + painter.setPen(itemColor); + painter.drawText(itemRect, Qt::AlignVCenter, itemString); + itemRect.translate(itemWidth(), 0); + + if (ctx.file == DiffFile::A) { + if (itemAddr == cursor.address) { + itemCursor.cachedChar = itemString.at(0); + itemCursor.cachedColor = itemColor; + } + } + + if (ctx.file == DiffFile::B) { + if (itemAddr == getAddressB(cursor.address)) { + itemCursor.cachedCharB = itemString.at(0); + itemCursor.cachedColorB = itemColor; + } + } + } + itemRect.translate(columnSpacingWidth(), 0); + } + itemRect.translate(0, lineHeight); + } + + painter.setPen(borderColor); + + const qreal vLineOffset = ctx.asciiArea.left() - charWidth; + painter.drawLine(QLineF(vLineOffset, 0, vLineOffset, viewport()->height())); +} + +void HexDiff::drawAsciiArea(QPainter &painter, DiffFileContext &ctx) +{ + const uint64_t addr = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + QRectF charRect(ctx.asciiArea.topLeft(), QSizeF(charWidth, lineHeight)); + + fillSelectionBackground(painter, ctx, true); + painter.setBrush(Qt::NoBrush); + + uint64_t address = addr; + QChar ascii; + QColor color; + auto &itemCursor = cursorArea < 2 ? cursor : shadowCursor; + for (int line = 0; line < visibleLines; ++line, charRect.translate(0, lineHeight)) { + charRect.moveLeft(ctx.asciiArea.left()); + for (int j = 0; j < itemRowByteLen() && address <= ctx.data->maxIndex(); ++j, ++address) { + ascii = renderAscii(address - addr, ctx, &color); + if (selection.contains(address) && cursorArea < 2) { + color = palette().highlightedText().color(); + } + if (ctx.file == DiffFile::A && diffItemsAt(address)) { + color = diffColor; + } + + if (ctx.file == DiffFile::B && diffItemsAt(getAddressA(address))) { + color = diffColor; + } + painter.setPen(color); + /* Dots look ugly. Use fillRect() instead of drawText(). */ + if (ascii == '.') { + const qreal a = cursor.screenPos.width(); + QPointF p = charRect.bottomLeft(); + p.rx() += (charWidth - a) / 2 + 1; + p.ry() += -2 * a; + painter.fillRect(QRectF(p, QSizeF(a, a)), color); + } else { + painter.drawText(charRect, Qt::AlignVCenter, ascii); + } + charRect.translate(charWidth, 0); + if (ctx.file == DiffFile::A && cursor.address == address) { + itemCursor.cachedChar = ascii; + itemCursor.cachedColor = color; + } + if (ctx.file == DiffFile::B && getAddressB(cursor.address) == address) { + itemCursor.cachedCharB = ascii; + itemCursor.cachedColorB = color; + } + } + } +} + +void HexDiff::fillSelectionBackground(QPainter &painter, DiffFileContext &ctx, bool ascii) +{ + if (selection.isEmpty()) { + return; + } + const auto parts = + rangePolygons(ctxSelection(ctx).start(), ctxSelection(ctx).end(), ascii, ctx); + for (const auto &shape : parts) { + const QColor highlightColor = palette().color(QPalette::Highlight); + if (ascii == (cursorArea < 2)) { + painter.setBrush(highlightColor); + painter.drawPolygon(shape); + } else { + painter.setPen(highlightColor); + painter.drawPolyline(shape); + } + } +} + +QVector HexDiff::rangePolygons(RVA start, RVA last, bool ascii, DiffFileContext &ctx) +{ + const uint64_t addr = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + + if (last < addr || start > lastVisibleAddr(ctx.file)) { + return {}; + } + + QRectF rect; + const QRectF area = QRectF(ascii ? ctx.asciiArea : ctx.itemArea); + + /* Convert absolute values to relative */ + const int startOffset = std::max(uint64_t(start), addr) - addr; + const int endOffset = std::min(uint64_t(last), lastVisibleAddr(ctx.file)) - addr; + + QVector parts; + + auto getRectangle = [&](int offset) { + return QRectF(ascii ? asciiRectangle(offset, ctx) : itemRectangle(offset, ctx)); + }; + + auto startRect = getRectangle(startOffset); + auto endRect = getRectangle(endOffset); + bool startJagged = false; + bool endJagged = false; + if (!ascii) { + if (const int startFraction = startOffset % itemByteLen) { + startRect.setLeft(startRect.left() + startFraction * startRect.width() / itemByteLen); + startJagged = true; + } + if (const int endFraction = itemByteLen - 1 - (endOffset % itemByteLen)) { + endRect.setRight(endRect.right() - endFraction * endRect.width() / itemByteLen); + endJagged = true; + } + } + if (endOffset - startOffset + 1 <= rowSizeBytes) { + if (startOffset / rowSizeBytes == endOffset / rowSizeBytes) { // single row + rect = startRect; + rect.setRight(endRect.right()); + parts.push_back(QPolygonF(rect)); + } else { + // two separate rectangles + rect = startRect; + rect.setRight(area.right()); + parts.push_back(QPolygonF(rect)); + rect = endRect; + rect.setLeft(area.left()); + parts.push_back(QPolygonF(rect)); + } + } else { + // single multiline shape + QPolygonF shape; + shape << startRect.topLeft(); + rect = getRectangle(startOffset + rowSizeBytes - 1 - startOffset % rowSizeBytes); + shape << rect.topRight(); + if (endOffset % rowSizeBytes != rowSizeBytes - 1) { + rect = getRectangle(endOffset - endOffset % rowSizeBytes - 1); + shape << rect.bottomRight() << endRect.topRight(); + } + shape << endRect.bottomRight(); + shape << getRectangle(endOffset - endOffset % rowSizeBytes).bottomLeft(); + if (startOffset % rowSizeBytes) { + rect = getRectangle(startOffset - startOffset % rowSizeBytes + rowSizeBytes); + shape << rect.topLeft() << startRect.bottomLeft(); + } + shape << shape.first(); // close the shape + parts.push_back(shape); + } + if (!ascii && (startJagged || endJagged) && parts.length() >= 1) { + + QPolygonF top; + top.reserve(3); + top << QPointF(0, 0) << QPointF(charWidth, lineHeight / 3) << QPointF(0, lineHeight / 2); + QPolygonF bottom; + bottom.reserve(3); + bottom << QPointF(0, lineHeight / 2) << QPointF(-charWidth, 2 * lineHeight / 3) + << QPointF(0, lineHeight); + + // small adjustment to make sure that edges don't overlap with rect edges, QPolygonF doesn't + // handle it properly + const QPointF adjustment(charWidth / 16, 0); + top.translate(-adjustment); + bottom.translate(adjustment); + + if (startJagged) { + auto movedTop = top.translated(startRect.topLeft()); + auto movedBottom = bottom.translated(startRect.topLeft()); + parts[0] = parts[0].subtracted(movedTop).united(movedBottom); + } + if (endJagged) { + auto movedTop = top.translated(endRect.topRight()); + auto movedBottom = bottom.translated(endRect.topRight()); + parts.last() = parts.last().subtracted(movedBottom).united(movedTop); + } + } + return parts; +} + +void HexDiff::updateMetrics() +{ + const QFontMetricsF fontMetrics(this->monospaceFont); + lineHeight = fontMetrics.height(); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + charWidth = fontMetrics.width('A'); +#else + charWidth = fontMetrics.horizontalAdvance('A'); +#endif + + updateCounts(); + updateAreasHeight(); + + const qreal cursorWidth = std::max(charWidth / 3, 1.); + cursor.screenPos.setHeight(lineHeight); + shadowCursor.screenPos.setHeight(lineHeight); + + cursor.screenPos.setWidth(cursorWidth); + + cursor.screenPosB.setHeight(lineHeight); + shadowCursor.screenPosB.setHeight(lineHeight); + + cursor.screenPosB.setWidth(cursorWidth); + if (cursorArea < 2) { + cursor.screenPos.moveTopLeft(ctxA.asciiArea.topLeft()); + + shadowCursor.screenPos.setWidth(itemWidth()); + shadowCursor.screenPos.moveTopLeft(ctxA.itemArea.topLeft()); + + cursor.screenPosB.moveTopLeft(ctxB.asciiArea.topLeft()); + + shadowCursor.screenPosB.setWidth(itemWidth()); + shadowCursor.screenPosB.moveTopLeft(ctxB.itemArea.topLeft()); + } else { + cursor.screenPos.moveTopLeft(ctxA.itemArea.topLeft()); + shadowCursor.screenPos.setWidth(charWidth); + shadowCursor.screenPos.moveTopLeft(ctxA.asciiArea.topLeft()); + + cursor.screenPosB.moveTopLeft(ctxB.itemArea.topLeft()); + shadowCursor.screenPosB.setWidth(charWidth); + shadowCursor.screenPosB.moveTopLeft(ctxB.asciiArea.topLeft()); + } +} + +void HexDiff::updateAreasPosition() +{ + const qreal spacingWidth = areaSpacingWidth(); + + const qreal yOffset = showHeader ? lineHeight : 0; + + // This feels soo reduntant will be fixing once cutter diff is done + ctxA.addrArea.setTopLeft(QPointF(0, yOffset)); + ctxA.addrArea.setWidth((addrCharLen + (showExAddr ? 2 : 0)) * charWidth); + + ctxA.itemArea.setTopLeft(QPointF(ctxA.addrArea.right() + spacingWidth, yOffset)); + ctxA.itemArea.setWidth(itemRowWidth()); + + ctxA.asciiArea.setTopLeft(QPointF(ctxA.itemArea.right() + spacingWidth, yOffset)); + ctxA.asciiArea.setWidth(asciiRowWidth()); + + ctxB.addrArea.setTopLeft(QPointF(ctxA.asciiArea.right() + spacingWidth, yOffset)); + ctxB.addrArea.setWidth((addrCharLen + (showExAddr ? 2 : 0)) * charWidth); + + ctxB.itemArea.setTopLeft(QPointF(ctxB.addrArea.right() + spacingWidth, yOffset)); + ctxB.itemArea.setWidth(itemRowWidth()); + + ctxB.asciiArea.setTopLeft(QPointF(ctxB.itemArea.right() + spacingWidth, yOffset)); + ctxB.asciiArea.setWidth(asciiRowWidth()); + + updateWidth(); +} + +void HexDiff::updateAreasHeight() +{ + visibleLines = static_cast((viewport()->height() - ctxA.itemArea.top()) / lineHeight); + + const qreal height = visibleLines * lineHeight; + ctxA.addrArea.setHeight(height); + ctxA.itemArea.setHeight(height); + ctxA.asciiArea.setHeight(height); + ctxB.addrArea.setHeight(height); + ctxB.itemArea.setHeight(height); + ctxB.asciiArea.setHeight(height); +} + +bool HexDiff::moveCursor(int offset, bool select, OverflowMove overflowMove) +{ + const uint64_t maxIndex = qMin(ctxA.data->maxIndex(), ctxB.data->maxIndex()); + BasicDiffCursor addr(cursor.address); + if (overflowMove == OverflowMove::Ignore) { + if (addr.moveChecked(offset)) { + if (addr.address > maxIndex) { + addr.address = maxIndex; + addr.pastEnd = true; + } + setCursorAddr(addr, select); + return true; + } + return false; + } else { + addr += offset; + if (addr.address > maxIndex) { + addr.address = maxIndex; + } + setCursorAddr(addr, select); + return true; + } +} + +void HexDiff::setCursorAddr(BasicDiffCursor addr, bool select) +{ + if (!select) { + const bool clearingSelection = !selection.isEmpty(); + selection.init(addr); + if (clearingSelection) { + emit selectionChanged(getSelection()); + } + } + emit positionChanged(addr.address); + + cursor.address = addr.address; + if (cursorArea > 1) { + cursor.address -= cursor.address % itemByteLen; + } + + /* Pause cursor repainting */ + cursorEnabled = false; + + if (select) { + selection.update(addr); + emit selectionChanged(getSelection()); + } + + uint64_t addressValue = cursor.address; + /* Update data cache if necessary */ + if (!(addressValue >= startAddress && addressValue <= lastVisibleAddr(DiffFile::A))) { + /* Align start address */ + addressValue -= (addressValue % itemRowByteLen()); + + /* FIXME: handling Page Up/Down */ + const uint64_t rowAfterVisibleAddress = startAddress + bytesPerScreen(); + if (addressValue == rowAfterVisibleAddress && addressValue > startAddress) { + // when pressing down add only one new row + startAddress += itemRowByteLen(); + } else { + startAddress = addressValue; + } + + fetchData(); + + if (startAddress > (ctxA.data->maxIndex() - bytesPerScreen()) + 1) { + startAddress = (ctxA.data->maxIndex() - bytesPerScreen()) + 1; + } + } + + updateCursorMeta(); + + /* Draw cursor */ + cursor.isVisible = !select; + updateViewport(); + + /* Resume cursor repainting */ + cursorEnabled = selection.isEmpty(); +} + +void HexDiff::updateCursorMeta() +{ + QPointF point; + QPointF pointAscii; + + const int offset = cursor.address - startAddress; + int itemOffset = offset; + int asciiOffset; + + /* Calc common Y coordinate */ + point.ry() = (itemOffset / itemRowByteLen()) * lineHeight; + pointAscii.setY(point.y()); + itemOffset %= itemRowByteLen(); + asciiOffset = itemOffset; + + /* Calc X coordinate on the item area */ + point.rx() = (itemOffset / itemGroupByteLen()) * columnExWidth(); + itemOffset %= itemGroupByteLen(); + point.rx() += (itemOffset / itemByteLen) * itemWidth(); + + /* Calc X coordinate on the ascii area */ + pointAscii.rx() = asciiOffset * charWidth; + + QPointF pointB = point; + QPointF pointAsciiB = pointAscii; + + point += ctxA.itemArea.topLeft(); + pointAscii += ctxA.asciiArea.topLeft(); + pointB += ctxB.itemArea.topLeft(); + pointAsciiB += ctxB.asciiArea.topLeft(); + + cursor.screenPos.moveTopLeft(cursorArea < 2 ? pointAscii : point); + shadowCursor.screenPos.moveTopLeft(cursorArea < 2 ? point : pointAscii); + cursor.screenPosB.moveTopLeft(cursorArea < 2 ? pointAsciiB : pointB); + shadowCursor.screenPosB.moveTopLeft(cursorArea < 2 ? pointB : pointAsciiB); +} + +void HexDiff::setCursorOnAscii(bool ascii) +{ + if (cursorArea % 2) { + if (ascii) { + cursorArea = DiffArea::AsciiB; + } else { + cursorArea = DiffArea::ItemB; + } + } else { + if (ascii) { + cursorArea = DiffArea::AsciiB; + } else { + cursorArea = DiffArea::ItemB; + } + } +} + +void HexDiff::setCursorOnArea(DiffArea area) +{ + cursorArea = area; +} + +QColor HexDiff::itemColor(uint8_t byte) +{ + QColor color(defColor); + // don't think this is relevant for diffing but keeping it + if (byte == 0x00) { + color = b0x00Color; + } else if (byte == 0x7f) { + color = b0x7fColor; + } else if (byte == 0xff) { + color = b0xffColor; + } else if (IS_PRINTABLE(byte)) { + color = printableColor; + } + + return color; +} + +template +static T fromBigEndian(const void *src) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + return qFromBigEndian(src); +#else + T result; + memcpy(&result, src, sizeof(T)); + return qFromBigEndian(result); +#endif +} + +template +static T fromLittleEndian(const void *src) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + return qFromLittleEndian(src); +#else + T result; + memcpy(&result, src, sizeof(T)); + return qFromLittleEndian(result); +#endif +} + +QVariant HexDiff::readItem(int offset, DiffFileContext &ctx, QColor *color) +{ + const uint64_t addr = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + quint8 byte; + quint16 word; + quint32 dword; + quint64 qword; + float float32; + double float64; + + quint8 bytes[sizeof(uint64_t)]; + ctx.data->copy(bytes, addr + offset, static_cast(itemByteLen)); + const bool signedItem = itemFormat == ItemFormatSignedDec; + + if (color) { + *color = defColor; + } + + switch (itemByteLen) { + case 1: + byte = bytes[0]; + if (color) { + *color = itemColor(byte); + } + if (!signedItem) { + return QVariant(static_cast(byte)); + } + return QVariant(static_cast(static_cast(byte))); + case 2: + if (itemBigEndian) { + word = fromBigEndian(bytes); + } else { + word = fromLittleEndian(bytes); + } + + if (!signedItem) { + return QVariant(static_cast(word)); + } + return QVariant(static_cast(static_cast(word))); + case 4: + if (itemBigEndian) { + dword = fromBigEndian(bytes); + } else { + dword = fromLittleEndian(bytes); + } + + if (itemFormat == ItemFormatFloat) { + memcpy(&float32, &dword, sizeof(float32)); + return QVariant(float32); + } + if (!signedItem) { + return QVariant(static_cast(dword)); + } + return QVariant(static_cast(static_cast(dword))); + case 8: + if (itemBigEndian) { + qword = fromBigEndian(bytes); + } else { + qword = fromLittleEndian(bytes); + } + if (itemFormat == ItemFormatFloat) { + memcpy(&float64, &qword, sizeof(float64)); + return QVariant(float64); + } + if (!signedItem) { + return QVariant(qword); + } + return QVariant(static_cast(qword)); + } + + return QVariant(); +} + +QString HexDiff::renderItem(int offset, DiffFileContext &ctx, QColor *color) +{ + QString item; + const QVariant itemVal = readItem(offset, ctx, color); + const int itemLen = itemCharLen - itemPrefixLen; /* Reserve space for prefix */ + + // FIXME: handle broken itemVal ( QVariant() ) + switch (itemFormat) { + case ItemFormatHex: + item = QString("%1").arg(itemVal.toULongLong(), itemLen, 16, QLatin1Char('0')); + if (itemByteLen > 1 && showExHex) { + item.prepend(hexPrefix); + } + break; + case ItemFormatOct: + item = QString("%1").arg(itemVal.toULongLong(), itemLen, 8, QLatin1Char('0')); + break; + case ItemFormatDec: + item = QString("%1").arg(itemVal.toULongLong(), itemLen, 10); + break; + case ItemFormatSignedDec: + item = QString("%1").arg(itemVal.toLongLong(), itemLen, 10); + break; + case ItemFormatFloat: + item = QString("%1").arg(itemVal.toDouble(), itemLen, 'g', itemByteLen == 4 ? 6 : 15); + break; + } + + return item; +} + +QChar HexDiff::renderAscii(int offset, DiffFileContext &ctx, QColor *color) +{ + const uint64_t addr = ctx.file == DiffFile::A ? startAddress : getStartAddressB(); + uchar byte; + ctx.data->copy(&byte, addr + offset, sizeof(byte)); + if (color) { + *color = itemColor(byte); + } + if (!IS_PRINTABLE(byte)) { + byte = '.'; + } + return QChar(byte); +} + +/** + * @brief Gets the available flags and comment at a specific address. + * @param address Address of Item to be checked. + * @return String containing the flags and comment available at the address. + */ +QString HexDiff::getFlagsAndComment(uint64_t address, DiffFileContext &ctx) +{ // Needs to be redefined using BinDiffClass so that it can be used for both cores + const QString flagNames = cutterDiff->listFlagsAsStringAt(address, ctx.file == DiffFile::A); + QString metaData = flagNames.isEmpty() ? "" : "Flags: " + flagNames.trimmed(); + + const QString comment = cutterDiff->getCommentAt(address, ctx.file == DiffFile::A); + if (!comment.isEmpty()) { + if (!metaData.isEmpty()) { + metaData.append("\n"); + } + metaData.append("Comment: " + comment.trimmed()); + } + + return metaData; +} + +void HexDiff::fetchData() +{ + ctxA.data->fetch(startAddress, bytesPerScreen()); + ctxB.data->fetch(getStartAddressB(), bytesPerScreen()); +} + +const QRectF &HexDiff::screenPosToArea(const QPoint &point) const +{ + if (ctxA.itemArea.contains(point)) { + return ctxA.itemArea; + } else if (ctxA.asciiArea.contains(point)) { + return ctxA.asciiArea; + } else if (ctxB.itemArea.contains(point)) { + return ctxB.itemArea; + } else if (ctxB.asciiArea.contains(point)) { + return ctxB.asciiArea; + } + return ctxA.asciiArea; +} + +BasicDiffCursor HexDiff::screenPosToAddrA(const QPoint &point, bool middle, int *wordOffset) const +{ + QPointF pt = point - screenPosToArea(point).topLeft(); + + int relativeAddress = 0; + const int line = static_cast(pt.y() / lineHeight); + relativeAddress += line * itemRowByteLen(); + const int column = static_cast(pt.x() / columnExWidth()); + relativeAddress += column * itemGroupByteLen(); + pt.rx() -= column * columnExWidth(); + auto roundingOffset = middle ? itemWidth() / 2 : 0; + int posInGroup = static_cast((pt.x() + roundingOffset) / itemWidth()); + if (!middle) { + posInGroup = std::min(posInGroup, itemGroupSize - 1); + } + relativeAddress += posInGroup * itemByteLen; + pt.rx() -= posInGroup * itemWidth(); + BasicDiffCursor result(startAddress); + result += relativeAddress; + + if (!middle && wordOffset != nullptr) { + int charPos = static_cast((pt.x() / charWidth) + 0.5); + charPos -= itemPrefixLen; + charPos = std::max(0, charPos); + *wordOffset = charPos; + } + return result; +} + +BasicDiffCursor HexDiff::asciiPosToAddrA(const QPoint &point, bool middle) const +{ + const QPointF pt = point - screenPosToArea(point).topLeft(); + + int relativeAddress = 0; + relativeAddress += static_cast(pt.y() / lineHeight) * itemRowByteLen(); + auto roundingOffset = middle ? (charWidth / 2) : 0; + relativeAddress += static_cast((pt.x() + (roundingOffset)) / charWidth); + BasicDiffCursor result(startAddress); + result += relativeAddress; + + return result; +} + +BasicDiffCursor HexDiff::currentAreaPosToAddrA(const QPoint &point, bool middle) const +{ + return cursorArea < 2 ? asciiPosToAddrA(point, middle) : screenPosToAddrA(point, middle); +} + +BasicDiffCursor HexDiff::mousePosToAddrA(const QPoint &point, bool middle) const +{ + return (ctxA.asciiArea.contains(point) || ctxB.asciiArea.contains(point)) + ? asciiPosToAddrA(point, middle) + : screenPosToAddrA(point, middle); +} + +QRectF HexDiff::itemRectangle(int offset, DiffFileContext &ctx) +{ + qreal x; + qreal y; + + qreal width = itemWidth(); + y = (offset / itemRowByteLen()) * lineHeight; + offset %= itemRowByteLen(); + + x = (offset / itemGroupByteLen()) * columnExWidth(); + offset %= itemGroupByteLen(); + x += (offset / itemByteLen) * itemWidth(); + if (offset == 0) { + x -= charWidth / 2; + width += charWidth / 2; + } + if (static_cast(offset) == itemGroupByteLen() - 1) { + width += charWidth / 2; + } + + x += ctx.itemArea.x(); + y += ctx.itemArea.y(); + + return QRectF(x, y, width, lineHeight); +} + +QRectF HexDiff::asciiRectangle(int offset, DiffFileContext &ctx) +{ + QPointF p; + + p.ry() = (offset / itemRowByteLen()) * lineHeight; + offset %= itemRowByteLen(); + + p.rx() = offset * charWidth; + + p += ctx.asciiArea.topLeft(); + + return QRectF(p, QSizeF(charWidth, lineHeight)); +} + +RVA HexDiff::getLocationAddress() +{ + return !selection.isEmpty() ? selection.start() : cursor.address; +} + +void HexDiff::updateViewport() +{ + vScrollBar->setPosition(startAddress); + viewport()->update(); +} + +void HexDiff::scrollLines(int lines, bool clampToScrollBarRange) +{ + const uint64_t maxIndex = qMin(ctxA.data->maxIndex(), ctxB.data->maxIndex()); + const int64_t delta = -lines * itemRowByteLen(); + + if (lines == 0) { + return; + } + + if (delta < 0 && startAddress < static_cast(-delta)) { + startAddress = 0; + } else if (delta > 0 && maxIndex < static_cast(bytesPerScreen())) { + startAddress = 0; + } else if ((maxIndex - startAddress) <= static_cast(bytesPerScreen() + delta - 1)) { + startAddress = (maxIndex - bytesPerScreen()) + 1; + } else { + startAddress += delta; + } + + if (clampToScrollBarRange) { + startAddress = vScrollBar->clampAddressToRange(startAddress); + } + fetchData(); + + updateCursorStatus(); + updateViewport(); +} + +void HexDiff::setStartAddress(RVA address) +{ + RVA aligned = address - (address % itemByteLen); + + const uint64_t maxIdx = qMin(ctxA.data->maxIndex(), ctxB.data->maxIndex()); + const uint64_t screenBytes = bytesPerScreen(); + if (maxIdx > screenBytes) { + RVA maxStart = (maxIdx - screenBytes + 1); + maxStart -= (maxStart % itemByteLen); + aligned = std::min(aligned, maxStart); + } else { + aligned = 0; + } + + // if (aligned == startAddress) { + // return; + // } + startAddress = aligned; + fetchData(); + + updateCursorStatus(); + updateViewport(); +} + +void HexDiff::shiftStartAddress(int shift) +{ + if (shift < 0) { + setStartAddress(startAddress - qAbs(shift)); + return; + } + setStartAddress(startAddress + shift); +} + +void HexDiff::transpose(int transA, int transB, bool reset) +{ // Number of bits + if (reset) { + relTranspose = 0; + } + transA /= itemByteLen; + transB /= itemByteLen; + relTranspose += transB * itemByteLen - transA * itemByteLen; + shiftStartAddress(transA * itemByteLen); + clearSelection(); + moveCursor(-transB); + updateViewport(); +} + +void HexDiff::updateCursorStatus() +{ + if (cursor.address >= startAddress && cursor.address <= lastVisibleAddr(DiffFile::A)) { + /* Don't enable cursor blinking if selection isn't empty */ + cursorEnabled = selection.isEmpty(); + updateCursorMeta(); + } else { + cursorEnabled = false; + } +} diff --git a/src/tools/bindiff/HexDiff.h b/src/tools/bindiff/HexDiff.h new file mode 100644 index 0000000000..d768f5fa7f --- /dev/null +++ b/src/tools/bindiff/HexDiff.h @@ -0,0 +1,568 @@ +#ifndef HEXDIFF_H +#define HEXDIFF_H + +#include "Cutter.h" +#include "common/IOModesController.h" +#include "dialogs/HexdumpRangeDialog.h" +#include "widgets/HexWidget.h" +// #include "tools/bindiff/HexDiffWidget.h" + +#include +#include +#include + +#include +#include + +class MemoryDiffData +{ +public: + MemoryDiffData(CutterDiff *cutterDiff, bool orig) : cutterDiff(cutterDiff), orig(orig) {} + ~MemoryDiffData() {} + static constexpr size_t blockSize = 4096; + + void fetch(uint64_t address, int length) + { + // FIXME: reuse data if possible + // Will be fixing this since for diffing the core files needs efficient cache management + const uint64_t blockSize = 0x1000ULL; + const uint64_t alignedAddr = address & ~(blockSize - 1); + const int offset = address - alignedAddr; + int len = (offset + length + (blockSize - 1)) & ~(blockSize - 1); + mFirstBlockAddr = alignedAddr; + mLastValidAddr = length ? alignedAddr + len - 1 : 0; + if (mLastValidAddr < mFirstBlockAddr) { + mLastValidAddr = -1; + len = mLastValidAddr - mFirstBlockAddr + 1; + } + mBlocks.clear(); + uint64_t addr = alignedAddr; + for (ut64 i = 0; i < len / blockSize; ++i, addr += blockSize) { + mBlocks.append(cutterDiff->ioRead(addr, blockSize, orig)); + } + } + + bool copy(void *out, uint64_t addr, size_t len) + { + if (addr < mFirstBlockAddr + || addr > mLastValidAddr + /* do not merge with previous check to handle overflows */ + || (mLastValidAddr - addr + 1) < len || mBlocks.isEmpty()) { + memset(out, 0xff, len); + return false; + } + + const int totalOffset = addr - mFirstBlockAddr; + const int blockId = totalOffset / blockSize; + const int blockOffset = totalOffset % blockSize; + const size_t firstPart = blockSize - blockOffset; + if (firstPart >= len) { + memcpy(out, mBlocks.at(blockId).constData() + blockOffset, len); + } else { + memcpy(out, mBlocks.at(blockId).constData() + blockOffset, firstPart); + memcpy(static_cast(out) + firstPart, mBlocks.at(blockId + 1).constData(), + len - firstPart); + } + return true; + } + + static uint64_t maxIndex() { return std::numeric_limits::max(); } + + uint64_t minIndex() const { return mFirstBlockAddr; } + +private: + CutterDiff *cutterDiff; + bool orig = true; + QVector mBlocks; + uint64_t mFirstBlockAddr = 0; + uint64_t mLastValidAddr = 0; +}; + +// Defining DiffFile Struct for encapsulation(in some external calls bool is used instead ex: seek) +enum DiffFile : ut8 { A, B }; + +enum DiffArea : ut8 { + AsciiA, + AsciiB, + ItemA, + ItemB +}; // used modulo and comparisons for getting areas for cursorArea and area in mouse press event + // both need to be rewritten with a nullarea or no area pointer + +// Defining DiffFileContext sharing file specific elements in one unified struct +// I think this shall be moved to the HexDiff class Itself +class DiffFileContext +{ +public: + DiffFile file; + uint64_t startAddress; + std::unique_ptr data; + QRectF addrArea; + QRectF itemArea; + QRectF asciiArea; +}; + +// Kept as it was in HexWidget didn't felt the need to change +// The cursors main address space is that of file A. Any operations +// let it be file reading, data loading, cursor moving it happens relative to +// File A. The Corresponding File A address is translated to File B for File B operations +// Ex: getAddressB, getStartAddressB etc. +// Address specific Data variable only exists for A +// I think we need a virtual address space which will make things more understandable +// may be in future +/** + * @brief Tracks memory addresses while preventing 64-bit overflow + */ +struct BasicDiffCursor +{ + uint64_t address; + bool pastEnd; + explicit BasicDiffCursor(uint64_t pos) : address(pos), pastEnd(false) {} + BasicDiffCursor() : address(0), pastEnd(false) {} + BasicDiffCursor &operator+=(int64_t offset) + { + if (offset < 0 && uint64_t(-offset) > address) { + address = 0; + pastEnd = false; + } else if (offset > 0 && uint64_t(offset) > (UINT64_MAX - address)) { + address = UINT64_MAX; + pastEnd = true; + } else { + address += uint64_t(offset); + pastEnd = false; + } + return *this; + } + BasicDiffCursor &operator+=(int offset) + { + *this += int64_t(offset); + return *this; + } + + bool moveChecked(int offset) + { + auto oldAddress = address; + *this += offset; + return address - oldAddress == uint64_t(offset); + } + + BasicDiffCursor &operator+=(uint64_t offset) + { + if (uint64_t(offset) > (UINT64_MAX - address)) { + address = UINT64_MAX; + pastEnd = true; + } else { + address += offset; + pastEnd = false; + } + return *this; + } + bool operator<(const BasicDiffCursor &r) const + { + return address < r.address || (pastEnd < r.pastEnd); + } +}; + +/** + * @brief Controls the cursor's visual blinking and screen location + */ +struct HexDiffCursor +{ + HexDiffCursor() : isVisible(false), onAsciiArea(false) {} + + bool isVisible; + bool onAsciiArea; + QTimer blinkTimer; + QRectF screenPos; // since it is a canvas item two rectangles were needed + QRectF screenPosB; + uint64_t address; + QString cachedChar; + QString cachedCharB; + QColor cachedColor; + QColor cachedColorB; + + void blink() { isVisible = !isVisible; } + void setBlinkPeriod(int msec) { blinkTimer.setInterval(msec / 2); } + void startBlinking() { blinkTimer.start(); } + void stopBlinking() { blinkTimer.stop(); } +}; + +// As it was in HexWidget +class HexDiffSelection +{ +public: + HexDiffSelection() : empty(true) { mStart = mEnd = 0; } + + inline void init(BasicDiffCursor addr) + { + empty = true; + mInit = addr; + } + + void set(uint64_t start, uint64_t end) + { + empty = false; + mInit = BasicDiffCursor(start); + mStart = start; + mEnd = end; + } + + void update(BasicDiffCursor addr) + { + empty = false; + if (mInit < addr) { + mStart = mInit.address; + mEnd = addr.address; + if (!addr.pastEnd) { + mEnd -= 1; + } + } else if (addr < mInit) { + mStart = addr.address; + mEnd = mInit.address; + if (!mInit.pastEnd) { + mEnd -= 1; + } + } else { + mStart = mEnd = mInit.address; + empty = true; + } + } + + bool intersects(uint64_t start, uint64_t end) const + { + return !empty && mEnd >= start && mStart <= end; + } + + bool contains(uint64_t pos) const { return !empty && mStart <= pos && pos <= mEnd; } + + uint64_t size() const + { + uint64_t size = 0; + if (!isEmpty()) { + size = mEnd - mStart + 1; + } + return size; + } + + inline bool isEmpty() const { return empty; } + inline uint64_t start() const { return mStart; } + inline uint64_t end() const { return mEnd; } + +private: + BasicDiffCursor mInit; + uint64_t mStart; + uint64_t mEnd; + bool empty; +}; + +class AddressRangeScrollBar; + +/** + * @brief Widget for rendering and editing of hex and ASCII memory data + */ +class HexDiff : public QScrollArea +{ + Q_OBJECT + +public: + explicit HexDiff(CutterDiff *cutterDiff, QWidget *parent = nullptr); + ~HexDiff() override = default; + + void setMonospaceFont(const QFont &font); + + enum AddrWidth : ut8 { AddrWidth32 = 8, AddrWidth64 = 16 }; + enum ItemSize : ut8 { + ItemSizeByte = 1, + ItemSizeWord = 2, + ItemSizeDword = 4, + ItemSizeQword = 8 + }; + enum ItemFormat : ut8 { + ItemFormatHex, + ItemFormatOct, + ItemFormatDec, + ItemFormatSignedDec, + ItemFormatFloat + }; + + enum class ColumnMode : ut8 { Fixed, PowerOf2 }; + enum class EditWordState : ut8 { Read, WriteNotStarted, WriteNotEdited, WriteEdited }; + + void setItemSize(int nbytes); + void setItemFormat(ItemFormat format); + void setItemEndianness(bool bigEndian); + void setItemGroupSize(int size); + /** + * @brief Sets line size in bytes. + * Changes column mode to fixed. Command can be rejected if current item format is bigger than + * requested size. + * @param bytes line size in bytes. + */ + void setFixedLineSize(int bytes); + void setColumnMode(ColumnMode mode); + + /** + * @brief Select non empty inclusive range [start; end] + * @param start + * @param end + */ + void selectRange(RVA start, RVA end); + void clearSelection(); + + // used to shift itemElements similar to shift in rz-diff + void transpose(int transA = 0, int transB = 1, bool reset = false); + void shiftStartAddress(int shift); + + // Selection needed address Variables for B since it is accessed by external widgets as well. + struct Selection + { + bool empty; + RVA startAddress; + RVA endAddress; + RVA startAddressB; + RVA endAddressB; + }; + Selection getSelection(); +public slots: + void seek(uint64_t address, bool orig = true); + void refresh(); + void updateColors(); +signals: + void selectionChanged(HexDiff::Selection selection); + void positionChanged(RVA start); + +protected: + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + bool event(QEvent *event) override; + +private slots: + void onCursorBlinked(); + void onHexPairsModeEnabled(bool enable); + void copy(); + void copyAddress(); + void onRangeDialogAccepted(); + +private: + void updateItemLength(); + void updateCounts(); + // DiffFileContext was pased for each of these paint functions. Instead of two calls it has to + // be merged in a single function. ie a single function pains both A's and B's areas Functions + // such as rangePolygons and diffing will be easier and efficient if the paint events are + // coordinated + void drawHeader(QPainter &painter, DiffFileContext &ctx); + void drawCursor(QPainter &painter, bool shadow = false); + void drawAddrArea(QPainter &painter, DiffFileContext &ctx); + void drawItemArea(QPainter &painter, DiffFileContext &ctx); + void drawAsciiArea(QPainter &painter, DiffFileContext &ctx); + void fillSelectionBackground(QPainter &painter, DiffFileContext &ctx, bool ascii = false); + void updateMetrics(); + void updateAreasPosition(); + void updateAreasHeight(); + enum class OverflowMove : ut8 { Clamp, Ignore }; + bool moveCursor(int offset, bool select = false, + OverflowMove overflowMove = OverflowMove::Clamp); + void setCursorAddr(BasicDiffCursor addr, bool select = false); + void updateCursorMeta(); + void setCursorOnAscii(bool ascii); + void setCursorOnArea(DiffArea area); + QColor itemColor(uint8_t byte); + QVariant readItem(int offset, DiffFileContext &ctx, QColor *color = nullptr); + QString renderItem(int offset, DiffFileContext &ctx, QColor *color = nullptr); + QChar renderAscii(int offset, DiffFileContext &ctx, QColor *color = nullptr); + QString getFlagsAndComment(uint64_t address, DiffFileContext &ctx); + /** + * @brief Get the location on which operations such as Writing should apply. + * @return Start of selection if multiple bytes are selected. Otherwise, the curren seek of the + * widget. + */ + RVA getLocationAddress(); + + void fetchData(); + /** + * @brief Convert mouse position to address. + * @param point mouse position in widget + * @param middle start next position from middle of symbol. Use middle=true for vertical cursor + * position between symbols, middle=false for insert mode cursor and getting symbol under + * cursor. + * @return + */ + BasicDiffCursor screenPosToAddrA(const QPoint &point, bool middle = false, + int *wordOffset = nullptr) const; + BasicDiffCursor asciiPosToAddrA(const QPoint &point, bool middle = false) const; + BasicDiffCursor currentAreaPosToAddrA(const QPoint &point, bool middle = false) const; + BasicDiffCursor mousePosToAddrA(const QPoint &point, bool middle = false) const; + // Converts the point to corresponding DiffArea + DiffArea posToDiffArea(QPoint &point) const; + /** + * @brief Rectangle for single item in data area. + * @param offset relative to first byte on screen + * @return + */ + QRectF itemRectangle(int offset, DiffFileContext &file); + /** + * @brief Rectangle for single item in ascii area. + * @param offset relative to first byte on screen + * @return + */ + QRectF + asciiRectangle(int offset, + DiffFileContext &ctx); // selects the ascii rectangles based on the context + QVector + rangePolygons(RVA start, RVA last, bool ascii, + DiffFileContext &ctx); // selects the item rectangles based on the context + void updateWidth(); + + inline qreal itemWidth() const { return itemCharLen * charWidth; } + + inline int itemGroupCharLen() const { return itemCharLen * itemGroupSize; } + + inline int columnExCharLen() const { return itemGroupCharLen() + columnSpacing; } + + inline int itemGroupByteLen() const { return itemByteLen * itemGroupSize; } + + inline qreal columnWidth() const { return itemGroupCharLen() * charWidth; } + + inline qreal columnExWidth() const { return columnExCharLen() * charWidth; } + + inline qreal columnSpacingWidth() const { return columnSpacing * charWidth; } + + inline int itemRowCharLen() const { return itemColumns * columnExCharLen() - columnSpacing; } + + inline int itemRowByteLen() const { return rowSizeBytes; } + + inline int bytesPerScreen() const { return itemRowByteLen() * visibleLines; } + + inline qreal itemRowWidth() const { return itemRowCharLen() * charWidth; } + + inline qreal asciiRowWidth() const { return itemRowByteLen() * charWidth; } + + inline qreal areaSpacingWidth() const { return areaSpacing * charWidth; } + + inline uint64_t lastVisibleAddr(DiffFile file) const + { + return ((file == DiffFile::A ? startAddress : getStartAddressB()) - 1) + bytesPerScreen(); + } + + const QRectF ¤tArea(); + + const QRectF &screenPosToArea(const QPoint &point) const; + + bool isFixedWidth() const; + + void updateViewport(); + void scrollLines(int lines, bool clampToScrollBarRange = false); + /** + * @brief Sets the given address as the first visible address of the view + * No action is taken if the address is already at the top + * @param address Target RVA to display at the top + */ + void setStartAddress(RVA address); + + uint64_t getAddressB(uint64_t addrA) const + { + if (relTranspose < 0) { + return addrA - qAbs(relTranspose); + } + return addrA + relTranspose; + } + uint64_t getAddressA(uint64_t addrB) const + { + if (relTranspose < 0) { + return addrB + qAbs(relTranspose); + } + return addrB - relTranspose; + } + + uint64_t getStartAddressB() const { return getAddressB(startAddress); } + + /** + * @brief Updates cursor visibility and metadata + * The cursor is only visible if it is within the visible address range and selection is empty + */ + void updateCursorStatus(); + + bool cursorEnabled; + DiffArea cursorArea; + HexDiffCursor cursor; + HexDiffCursor shadowCursor; + + HexDiffSelection selection; + bool updatingSelection; + + int itemByteLen = 1; + int itemGroupSize = 1; ///< Items per group (default: 1), 2 in case of hexpair mode + int rowSizeBytes = 16; ///< Line size in bytes + int itemColumns = 16; ///< Number of columns, single column consists of itemGroupSize items + int itemCharLen = 2; + int itemPrefixLen = 0; + int relTranspose = 0; ///< relative transpose between the files(shift) + ColumnMode columnMode; + + ItemFormat itemFormat; + + bool itemBigEndian; + QString itemPrefix; + + int visibleLines; + uint64_t startAddress; + qreal charWidth; + qreal lineHeight; + int addrCharLen; + QFont monospaceFont; + + bool showHeader; + bool showAscii; + bool showExHex; + bool showExAddr; + + bool diffItemsAt(uint64_t addr) const; + + QColor borderColor; + QColor backgroundColor; + QColor defColor; + QColor addrColor; + QColor diffColor; + QColor b0x00Color; + QColor b0x7fColor; + QColor b0xffColor; + QColor printableColor; + + HexdumpRangeDialog rangeDialog; + + /* Spacings in characters */ + const int columnSpacing = 1; + const int areaSpacing = 2; + + const QString hexPrefix = QStringLiteral("0x"); + + QMenu *rowSizeMenu; + QAction *actionRowSizePowerOf2; + QList actionsItemSize; + QList actionsItemFormat; + QAction *actionItemBigEndian; + QAction *actionHexPairs; + QAction *actionCopy; + QAction *actionCopyAddress; + QAction *actionSelectRange; + + DiffFileContext ctxA; + DiffFileContext ctxB; + DiffFile getFileFromPos(QPoint &pos) const; + // Gives selection from particular context + HexDiffSelection ctxSelection(DiffFileContext &ctx); + // Gives address at given context + uint64_t ctxAddr(uint64_t addrA, DiffFileContext &ctx); + + CutterDiff *cutterDiff; + AddressRangeScrollBar *vScrollBar; +}; + +#endif // HEXWIDGET_H diff --git a/src/tools/bindiff/LineDiffWidget.cpp b/src/tools/bindiff/LineDiffWidget.cpp new file mode 100644 index 0000000000..0924c91399 --- /dev/null +++ b/src/tools/bindiff/LineDiffWidget.cpp @@ -0,0 +1,297 @@ +#include "LineDiffWidget.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +LineDiffWidget::LineDiffWidget(CutterDiff *cutterDiff, QWidget *parent) + : QWidget(parent), + cutterDiff(cutterDiff), + leftEdit(new DiffTextEdit(this)), + rightEdit(new DiffTextEdit(this)), + unifiedEdit(new DiffTextEdit(this)), + viewSelector(new QComboBox(this)), + splitViewSplitter(new QSplitter(Qt::Horizontal, this)) +{ + auto *layoutV = new QVBoxLayout(this); + layoutV->setContentsMargins(0, 0, 0, 0); + layoutV->setSpacing(0); + auto *layoutHEdits = new QHBoxLayout(this); + layoutV->addLayout(layoutHEdits); + layoutHEdits->addWidget(unifiedEdit); + + splitViewSplitter->addWidget(leftEdit); + splitViewSplitter->addWidget(rightEdit); + layoutHEdits->addWidget(splitViewSplitter); + auto layoutH = new QHBoxLayout(this); + layoutH->addStretch(); + auto labelLineDiff = new QLabel(this); + labelLineDiff->setText("View : "); + layoutH->addWidget(labelLineDiff); + viewSelector->addItems({ "Unified", "Split", "Left", "Right" }); + connect(viewSelector, &QComboBox::currentIndexChanged, this, + &LineDiffWidget::onViewModeChanged); + + layoutH->addWidget(viewSelector); + layoutV->addLayout(layoutH); + + connect(leftEdit->verticalScrollBar(), &QScrollBar::valueChanged, this, + [this](int value) { rightEdit->verticalScrollBar()->setValue(value); }); + + connect(rightEdit->verticalScrollBar(), &QScrollBar::valueChanged, this, + [this](int value) { leftEdit->verticalScrollBar()->setValue(value); }); + setUpFonts(); + connect(Config(), &Configuration::fontsUpdated, this, &LineDiffWidget::setUpFonts); + onViewModeChanged(); +} + +LineDiffWidget::~LineDiffWidget() {} + +void LineDiffWidget::onViewModeChanged() +{ + splitViewSplitter->hide(); + unifiedEdit->hide(); + leftEdit->hide(); + rightEdit->hide(); + switch (viewSelector->currentIndex()) { + case 0: + unifiedEdit->show(); + break; + case 1: { + splitViewSplitter->show(); + leftEdit->show(); + rightEdit->show(); + break; + } + case 2: { + splitViewSplitter->show(); + leftEdit->show(); + rightEdit->hide(); + break; + } + case 3: { + splitViewSplitter->show(); + leftEdit->hide(); + rightEdit->show(); + break; + } + default: + break; + } +} + +void LineDiffWidget::setUpFonts() +{ + leftEdit->setUpFont(Config()->getFont()); + rightEdit->setUpFont(Config()->getFont()); + unifiedEdit->setUpFont(Config()->getFont()); +} + +void LineDiffWidget::fetchFunctionDisasSplit(RVA addrA, RVA addrB) +{ + const QSignalBlocker blocker1(leftEdit), blocker2(rightEdit), blocker3(unifiedEdit); + leftEdit->clear(); + rightEdit->clear(); + char *stringUtf; + RzDiff *diffedLines = cutterDiff->diffFunctionDissas(addrA, addrB); + QColor matched = Config()->getColor("gui.match.perfect"); + QColor unmatched = Config()->getColor("gui.match.partial"); + matched.setAlpha(50); + unmatched.setAlpha(50); + const CutterRzList*/> groups = + cutterDiff->lineDiffOpsGrouped(diffedLines); + for (const RzList /**/ *group : groups) { + for (RzDiffOp *op : CutterRzList(group)) { + + switch (op->type) { + case RZ_DIFF_OP_EQUAL: { + stringUtf = rz_diff_op_stringify(diffedLines, op, true); + const QString opString = QString::fromUtf8(stringUtf); + leftEdit->insertFormatted(opString, { 0, 0, 0, 0 }); + rightEdit->insertFormatted(opString, { 0, 0, 0, 0 }); + unifiedEdit->insertFormatted(opString, { 0, 0, 0, 0 }); + break; + } + case RZ_DIFF_OP_DELETE: { + stringUtf = rz_diff_op_stringify(diffedLines, op, true); + leftEdit->insertFormatted(QString::fromUtf8(stringUtf), unmatched); + unifiedEdit->insertFormatted(QString::fromUtf8(stringUtf), unmatched); + break; + } + case RZ_DIFF_OP_INSERT: { + stringUtf = rz_diff_op_stringify(diffedLines, op, false); + rightEdit->insertFormatted(QString::fromUtf8(stringUtf), matched); + unifiedEdit->insertFormatted(QString::fromUtf8(stringUtf), matched); + break; + } + case RZ_DIFF_OP_REPLACE: { + const QString actual = + QString::fromUtf8(rz_diff_op_stringify(diffedLines, op, true)); + const QString replaced = + QString::fromUtf8(rz_diff_op_stringify(diffedLines, op, false)); + const auto bound = cutterDiff->getLineDiffBounds(actual, replaced); + leftEdit->insertBounded(actual, unmatched, bound); + rightEdit->insertBounded(replaced, matched, bound); + unifiedEdit->insertBounded(actual, unmatched, bound); + unifiedEdit->insertBounded(replaced, matched, bound); + break; + } + default: + break; + } + balanceLines(); + } + } + rz_diff_free(diffedLines); +} + +void LineDiffWidget::balanceLines() +{ + const int leftLines = leftEdit->document()->blockCount(); + const int rightLines = rightEdit->document()->blockCount(); + + if (leftLines == rightLines) { + return; + } + + DiffTextEdit *edit = (leftLines < rightLines) ? leftEdit : rightEdit; + const int missing = std::abs(leftLines - rightLines); + + QTextCursor cursor(edit->document()); + cursor.movePosition(QTextCursor::End); + + for (int i = 0; i < missing; ++i) { + cursor.insertBlock(); + } + + edit->setTextCursor(cursor); +} + +DiffTextEdit::DiffTextEdit(QWidget *parent) : QPlainTextEdit(parent) +{ + setReadOnly(true); + setTextInteractionFlags(Qt::TextSelectableByKeyboard | Qt::TextSelectableByMouse); + lineNumberArea = new LineNumberArea(this); + connect(this, &QPlainTextEdit::blockCountChanged, this, &DiffTextEdit::updateLineNumberArea); + + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, + &DiffTextEdit::updateLineNumberArea); + + connect(this, &QPlainTextEdit::cursorPositionChanged, this, + &DiffTextEdit::highlightCurrentLine); + setLineWrapMode(QPlainTextEdit::NoWrap); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); +} + +DiffTextEdit::~DiffTextEdit() {} + +void DiffTextEdit::highlightCurrentLine() +{ + const QTextBlock currentBlock = textCursor().block(); + + if (highlightedBlock.isValid() && highlightedBlock != currentBlock) { + QTextCursor oldCursor(highlightedBlock); + QTextBlockFormat oldFmt = highlightedBlock.blockFormat(); + oldFmt.clearBackground(); + oldCursor.setBlockFormat(oldFmt); + } + + QTextCursor cursor(currentBlock); + QTextBlockFormat fmt = currentBlock.blockFormat(); + fmt.setBackground(Config()->getColor("gui.background").lighter(100)); + cursor.setBlockFormat(fmt); + + highlightedBlock = currentBlock; +} + +void DiffTextEdit::resizeEvent(QResizeEvent *event) +{ + QPlainTextEdit::resizeEvent(event); + + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); + + lineNumberArea->setGeometry(0, 0, lineNumberAreaWidth(), height()); +} + +int DiffTextEdit::lineNumberAreaWidth() const +{ + const int digits = QString::number(document()->blockCount()).length(); + + return 8 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; +} + +void DiffTextEdit::updateLineNumberArea() +{ + setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); + lineNumberArea->update(); +} + +void DiffTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event) +{ + QPainter painter(lineNumberArea); + + painter.fillRect(event->rect(), Config()->getColor("gui.background").darker(115)); + + QTextBlock block = firstVisibleBlock(); + + while (block.isValid()) { + const int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top()); + const int bottom = top + qRound(blockBoundingRect(block).height()); + ; + + if (bottom >= event->rect().top() && top <= event->rect().bottom()) { + painter.setPen(Qt::darkGray); + + painter.drawText(0, top, lineNumberArea->width() - 4, fontMetrics().height(), + Qt::AlignRight, QString::number(block.blockNumber() + 1)); + } + block = block.next(); + } +} + +void DiffTextEdit::insertFormatted(const QString &text, const QColor &color) +{ + QTextCursor cursor = textCursor(); + cursor.movePosition(QTextCursor::End); + + const QTextCharFormat oldFormat = defaultFormat; + + QTextCharFormat fmt = oldFormat; + fmt.setBackground(color); + + cursor.insertText(text, fmt); + + cursor.setCharFormat(oldFormat); + setTextCursor(cursor); +} + +void DiffTextEdit::insertBounded(const QString &text, const QColor &color, const Bound bound) +{ + QTextCursor cursor = textCursor(); + cursor.movePosition(QTextCursor::End); + const QTextCharFormat oldFormat = defaultFormat; + const QColor colorHighlight = { color.red(), color.green(), color.blue(), 255 }; + QTextCharFormat fmt = oldFormat; + fmt.setBackground(color); + + cursor.insertText(text.left(bound.pos), fmt); + fmt.setBackground(colorHighlight); + cursor.insertText(text.mid(bound.pos, bound.size), fmt); + fmt.setBackground(color); + cursor.insertText(text.mid(bound.pos + bound.size), fmt); + cursor.setCharFormat(oldFormat); + setTextCursor(cursor); +} + +void DiffTextEdit::setUpFont(const QFont &font) +{ + setFont(font); + lineNumberArea->setFont(font); +} \ No newline at end of file diff --git a/src/tools/bindiff/LineDiffWidget.h b/src/tools/bindiff/LineDiffWidget.h new file mode 100644 index 0000000000..19f6d8a90e --- /dev/null +++ b/src/tools/bindiff/LineDiffWidget.h @@ -0,0 +1,79 @@ +#ifndef LINEDIFFWIDGET_H +#define LINEDIFFWIDGET_H + +#include +#include +#include +#include +#include +#include + +#include + +class DiffTextEdit; +class LineNumberArea; + +class LineDiffWidget : public QWidget +{ + Q_OBJECT + +public: + explicit LineDiffWidget(CutterDiff *cutterDiff, QWidget *parent = nullptr); + ~LineDiffWidget(); + void fetchFunctionDisasSplit(RVA addrA, RVA addrB); + void balanceLines(); + void setUpFonts(); + +protected: +private slots: + void onViewModeChanged(); + +private: + CutterDiff *cutterDiff; + DiffTextEdit *leftEdit; + DiffTextEdit *rightEdit; + DiffTextEdit *unifiedEdit; + QComboBox *viewSelector; + QSplitter *splitViewSplitter; +}; + +class DiffTextEdit : public QPlainTextEdit +{ + Q_OBJECT + +public: + explicit DiffTextEdit(QWidget *parent = nullptr); + ~DiffTextEdit(); + int lineNumberAreaWidth() const; + void lineNumberAreaPaintEvent(QPaintEvent *event); + void insertFormatted(const QString &text, const QColor &color = QColor()); + void insertBounded(const QString &text, const QColor &color, const Bound bound); + void setUpFont(const QFont &font); + +protected: + void resizeEvent(QResizeEvent *event) override; +private slots: + void updateLineNumberArea(); + +private: + LineNumberArea *lineNumberArea; + void highlightCurrentLine(); + const QTextCharFormat defaultFormat = textCursor().charFormat(); + QTextBlock highlightedBlock; +}; + +class LineNumberArea : public QWidget +{ +public: + explicit LineNumberArea(DiffTextEdit *editor) : QWidget(editor), editor(editor) {} + + QSize sizeHint() const override { return QSize(editor->lineNumberAreaWidth(), 0); } + +protected: + void paintEvent(QPaintEvent *event) override { editor->lineNumberAreaPaintEvent(event); } + +private: + DiffTextEdit *editor; +}; + +#endif // LINEDIFFWIDGET_H diff --git a/src/tools/bindiff/LineDiffWidget.ui b/src/tools/bindiff/LineDiffWidget.ui new file mode 100644 index 0000000000..a429bc7647 --- /dev/null +++ b/src/tools/bindiff/LineDiffWidget.ui @@ -0,0 +1,39 @@ + + + LineDiffWidget + + + + 0 + 0 + 774 + 529 + + + + Form + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + diff --git a/src/translations b/src/translations index fa68a9fec3..bc5aced406 160000 --- a/src/translations +++ b/src/translations @@ -1 +1 @@ -Subproject commit fa68a9fec31ad8b4a3bf8aab19de9c63d739c39e +Subproject commit bc5aced406678ae6f24fa501b53a7ae105c9f119 diff --git a/src/widgets/HexdumpWidget.ui b/src/widgets/HexdumpWidget.ui index 8e27de996a..fb6e6c892e 100644 --- a/src/widgets/HexdumpWidget.ui +++ b/src/widgets/HexdumpWidget.ui @@ -57,7 +57,7 @@ QTabWidget::TabPosition::North - 0 + 1 true