Skip to content

Commit

Permalink
Implement basic EMV highlighter for emv-viewer
Browse files Browse the repository at this point in the history
QSyntaxHighlighter is designed to process one block at a time with very
little context about other blocks. This is not ideal for EMV parsing but
appears to be the only way to apply text formatting without impacting
the undo/redo stack. Therefore, this implementation processes all blocks
and reparses all of the EMV data when updating each block. This
implementation also assumes that rehighlight() is called whenever the
widget text changes because changes to later blocks may invalidate EMV
field lengths specified in earlier blocks.
  • Loading branch information
leonlynch committed Jun 22, 2024
1 parent 40ec7af commit fcd1f95
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 4 deletions.
18 changes: 14 additions & 4 deletions viewer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,33 @@ endif()
find_package(QT 5.12 NAMES ${Qt_NAMES} REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets PATHS ${QT_DIR})
message(STATUS "Found Qt${QT_VERSION_MAJOR} Widgets: ${QT_CONFIG} (found suitable version \"${QT_VERSION}\")")
set(EMV_VIEWER_MOC_HEADERS
emv-viewer-mainwindow.h
emvhighlighter.h
betterplaintextedit.h
)
if(QT_VERSION VERSION_LESS 5.15)
# Qt-5.12 provides these versioned commands
qt5_wrap_ui(UI_SRCS emv-viewer-mainwindow.ui)
qt5_wrap_cpp(MOC_SRCS emv-viewer-mainwindow.h betterplaintextedit.h)
qt5_wrap_cpp(MOC_SRCS ${EMV_VIEWER_MOC_HEADERS})
add_library(Qt::Widgets ALIAS Qt5::Widgets)
else()
# Qt-5.15 and Qt-6 provide these version-less commands
qt_wrap_ui(UI_SRCS emv-viewer-mainwindow.ui)
qt_wrap_cpp(MOC_SRCS emv-viewer-mainwindow.h betterplaintextedit.h)
qt_wrap_cpp(MOC_SRCS ${EMV_VIEWER_MOC_HEADERS})
endif()

add_executable(emv-viewer emv-viewer.cpp emv-viewer-mainwindow.cpp ${UI_SRCS} ${MOC_SRCS} ${QRC_SRCS})
add_executable(emv-viewer
emv-viewer.cpp
emv-viewer-mainwindow.cpp
emvhighlighter.cpp
${UI_SRCS} ${MOC_SRCS} ${QRC_SRCS}
)
target_include_directories(emv-viewer PRIVATE
${CMAKE_CURRENT_BINARY_DIR} # For generated files
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> # For generated files to include source headers
)
target_link_libraries(emv-viewer Qt::Widgets)
target_link_libraries(emv-viewer Qt::Widgets emv::iso8825)

if(WIN32)
# Set properties needed for GUI applications on Windows
Expand Down
15 changes: 15 additions & 0 deletions viewer/emv-viewer-mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

#include "emv-viewer-mainwindow.h"
#include "emvhighlighter.h"

#include <QtCore/QStringLiteral>
#include <QtCore/QString>
Expand All @@ -44,6 +45,11 @@ EmvViewerMainWindow::EmvViewerMainWindow(QWidget* parent)
setupUi(this);
setWindowTitle(windowTitle().append(QStringLiteral(" (") + qApp->applicationVersion() + QStringLiteral(")")));

// Note that EmvHighlighter assumes that all blocks are processed in order
// for every change to the text. Therefore rehighlight() must be called
// whenever the widget text changes. See on_dataEdit_textChanged().
highlighter = new EmvHighlighter(dataEdit->document());

// Display copyright, license and disclaimer notice
descriptionText->appendHtml(QStringLiteral(
"Copyright 2021-2024 <a href='https://github.com/leonlynch'>Leon Lynch</a><br/><br/>"
Expand Down Expand Up @@ -148,6 +154,15 @@ void EmvViewerMainWindow::parseData()

void EmvViewerMainWindow::on_dataEdit_textChanged()
{
// Rehighlight when text changes. This is required because EmvHighlighter
// assumes that all blocks are processed in order for every change to the
// text. Note that rehighlight() will also re-trigger the textChanged()
// signal and therefore signals must be blocked for the duration of
// rehighlight().
dataEdit->blockSignals(true);
highlighter->rehighlight();
dataEdit->blockSignals(false);

// Bundle updates by restarting the timer every time the data changes
modelUpdateTimer->start(300);
}
Expand Down
2 changes: 2 additions & 0 deletions viewer/emv-viewer-mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
// Forward declarations
class QTimer;
class QStringListModel;
class EmvHighlighter;

class EmvViewerMainWindow : public QMainWindow, private Ui::MainWindow
{
Expand All @@ -53,6 +54,7 @@ private slots: // connect-by-name helper functions
protected:
QTimer* modelUpdateTimer;
QStringListModel* model;
EmvHighlighter* highlighter;
};

#endif
183 changes: 183 additions & 0 deletions viewer/emvhighlighter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* @file emvhighlighter.cpp
* @brief QSyntaxHighlighter derivative that applies highlighting to EMV data
*
* Copyright 2024 Leon Lynch
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "emvhighlighter.h"
#include "iso8825_ber.h"

#include <QtCore/QString>
#include <QtCore/QByteArray>
#include <QtGui/QTextDocument>
#include <QtGui/QTextBlock>
#include <QtGui/QTextCharFormat>
#include <QtGui/QColor>

#include <cstddef>
#include <cctype>

static bool parseBerData(
const void* ptr,
std::size_t len,
std::size_t* validBytes
)
{
int r;
struct iso8825_ber_itr_t itr;
struct iso8825_tlv_t tlv;
bool valid;

r = iso8825_ber_itr_init(ptr, len, &itr);
if (r) {
qWarning("iso8825_ber_itr_init() failed; r=%d", r);
return false;
}

while ((r = iso8825_ber_itr_next(&itr, &tlv)) > 0) {

if (iso8825_ber_is_constructed(&tlv)) {
// If the field is constructed, only consider the tag and length
// to be valid until the value has been parsed. The fields inside
// the value will be added when they are parsed.
*validBytes += (r - tlv.length);

// Recursively parse constructed fields
valid = parseBerData(tlv.value, tlv.length, validBytes);
if (!valid) {
qDebug("parseBerData() failed; validBytes=%zu", *validBytes);
return false;
}

} else {
// If the field is not constructed, consider all of the bytes to
// be valid BER encoded data
*validBytes += r;
}
}
if (r < 0) {
qDebug("iso8825_ber_itr_next() failed; r=%d", r);
return false;
}

return true;
}

class EmvTextBlockUserData : public QTextBlockUserData
{
public:
EmvTextBlockUserData(unsigned int startPos, unsigned int length)
: startPos(startPos),
length(length)
{}

public:
unsigned int startPos;
unsigned int length;
};

void EmvHighlighter::highlightBlock(const QString& text)
{
// QSyntaxHighlighter is designed to process one block at a time with very
// little context about other blocks. This is not ideal for EMV parsing
// but appears to be the only way to apply text formatting without
// impacting the undo/redo stack. Therefore, this implementation processes
// all blocks and reparses all of the EMV data when updating each block.
// This implementation also assumes that rehighlight() is called whenever
// the widget text changes because changes to later blocks may invalidate
// EMV field lengths specified in earlier blocks.

// NOTE: If anyone knows a better way, please let me know.

QTextDocument* doc = document();
int strLen = 0;
unsigned int validLen;
QString str;
QByteArray data;
std::size_t validBytes = 0;

// Concatenate all blocks without whitespace and compute start position
// and length of current block within concatenated string
for (QTextBlock block = doc->begin(); block != doc->end(); block = block.next()) {
QString blockStr = block.text().simplified().remove(' ');
if (currentBlock() == block) {
block.setUserData(new EmvTextBlockUserData(strLen, blockStr.length()));
}

strLen += blockStr.length();
str += blockStr;
}
if (strLen != str.length()) {
// Internal error
qWarning("strLen=%d; str.length()=%d", strLen, (int)str.length());
strLen = str.length();
}
validLen = strLen;

// Ensure that hex string contains only hex digits
for (unsigned int i = 0; i < validLen; ++i) {
if (!std::isxdigit(str[i].unicode())) {
// Only parse up to invalid digit
validLen = i;
break;
}
}

// Ensure that hex string has even number of digits
if (validLen & 0x01) {
// Odd number of digits. Ignore last digit to see whether parsing can
// proceed regardless and highlight error later.
validLen -= 1;
}

// Only decode valid hex digits to binary
data = QByteArray::fromHex(str.left(validLen).toUtf8());

// Parse BER encoded data and update number of valid characters
parseBerData(data.constData(), data.size(), &validBytes);
validLen = validBytes * 2;

EmvTextBlockUserData* blockData = static_cast<decltype(blockData)>(currentBlockUserData());
if (!blockData) {
// Internal error
qWarning("Block data missing for block number %d", currentBlock().blockNumber());
return;
}

if (validLen >= blockData->startPos + blockData->length) {
// All digits are valid
setFormat(0, currentBlock().length(), QTextCharFormat());

} else if (validLen <= blockData->startPos) {
// All digits are invalid
setFormat(0, currentBlock().length(), QColor(Qt::red));
} else {
// Some digits are invalid
unsigned int digitIdx = 0;
for (int i = 0; i < text.length(); ++i) {
if (blockData->startPos + digitIdx < validLen) {
setFormat(i, 1, QTextCharFormat());
} else {
setFormat(i, 1, QColor(Qt::red));
}

if (std::isxdigit(text[i].unicode())) {
++digitIdx;
}
}
}
}
41 changes: 41 additions & 0 deletions viewer/emvhighlighter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @file emvhighlighter.h
* @brief QSyntaxHighlighter derivative that applies highlighting to EMV data
*
* Copyright 2024 Leon Lynch
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#ifndef EMV_HIGHLIGHTER_H
#define EMV_HIGHLIGHTER_H

#include <QtGui/QSyntaxHighlighter>

// Forward declarations
class QTextDocument;

class EmvHighlighter : public QSyntaxHighlighter
{
Q_OBJECT

public:
explicit EmvHighlighter(QTextDocument* parent)
: QSyntaxHighlighter(parent)
{}

virtual void highlightBlock(const QString& text) override;
};

#endif

0 comments on commit fcd1f95

Please sign in to comment.