From 2da24be2e8f8ffa60e371557d75183b67fd2ea24 Mon Sep 17 00:00:00 2001
From: Toni Uhlig <matzeton@googlemail.com>
Date: Sun, 30 Jul 2023 20:55:52 +0200
Subject: Added UI to insert/delete bytes into a packet.

Signed-off-by: Toni Uhlig <matzeton@googlemail.com>
---
 CMakeLists.txt           |  10 ++++
 bytewindow.cpp           |  50 +++++++++++++++++
 bytewindow.h             |  31 +++++++++++
 bytewindow.ui            | 125 ++++++++++++++++++++++++++++++++++++++++++
 mainwindow.cpp           | 138 ++++++++++++++++++++++++++++++++++++++++++-----
 mainwindow.h             |  15 +++++-
 mainwindow.ui            |   6 +++
 qhexedit2/src/qhexedit.h |   3 +-
 8 files changed, 361 insertions(+), 17 deletions(-)
 create mode 100644 bytewindow.cpp
 create mode 100644 bytewindow.h
 create mode 100644 bytewindow.ui

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2df0801..88f7c74 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,7 +9,12 @@ set(CMAKE_AUTORCC ON)
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
+set(CPACK_PACKAGE_CONTACT "toni@impl.cc")
+set(CPACK_DEBIAN_PACKAGE_NAME "pcap-editor")
+set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
+
 include(ExternalProject)
+include(CPack)
 
 find_package(QT NAMES Qt6 REQUIRED COMPONENTS Core Widgets)
 find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets)
@@ -42,10 +47,15 @@ endif()
 
 set(PROJECT_SOURCES
         main.cpp
+
         mainwindow.cpp
         mainwindow.h
         mainwindow.ui
 
+        bytewindow.cpp
+        bytewindow.h
+        bytewindow.ui
+
         pcapplusplus.cpp
         pcapplusplus.h
 
diff --git a/bytewindow.cpp b/bytewindow.cpp
new file mode 100644
index 0000000..444399b
--- /dev/null
+++ b/bytewindow.cpp
@@ -0,0 +1,50 @@
+#include "bytewindow.h"
+#include "ui_bytewindow.h"
+
+ByteWindow::ByteWindow(QWidget *parent) :
+    QDialog(parent),
+    ui(new Ui::ByteWindow)
+{
+    ui->setupUi(this);
+}
+
+ByteWindow::~ByteWindow()
+{
+    delete ui;
+}
+
+void ByteWindow::set(ByteWindowOption option, int offset, int size)
+{
+    switch (option) {
+    case ByteWindowOption::BWO_UNKNOWN:
+    case ByteWindowOption::BWO_INSERT:
+        ui->radioButtonInsert->click();
+        break;
+    case ByteWindowOption::BWO_DELETE:
+        ui->radioButtonDelete->click();
+        break;
+    }
+
+    ui->spinBoxIndex->setValue(offset);
+    ui->spinBoxSize->setValue(size);
+}
+
+ByteWindowOption ByteWindow::getOption()
+{
+    if (ui->radioButtonInsert->isChecked())
+        return ByteWindowOption::BWO_INSERT;
+    if (ui->radioButtonDelete->isChecked())
+        return ByteWindowOption::BWO_DELETE;
+
+    return ByteWindowOption::BWO_UNKNOWN;
+}
+
+int ByteWindow::getOffset()
+{
+    return ui->spinBoxIndex->value();
+}
+
+int ByteWindow::getSize()
+{
+    return ui->spinBoxSize->value();
+}
diff --git a/bytewindow.h b/bytewindow.h
new file mode 100644
index 0000000..e087a1e
--- /dev/null
+++ b/bytewindow.h
@@ -0,0 +1,31 @@
+#ifndef BYTEWINDOW_H
+#define BYTEWINDOW_H
+
+#include <QDialog>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class ByteWindow; }
+QT_END_NAMESPACE
+
+enum class ByteWindowOption {
+    BWO_UNKNOWN,
+    BWO_INSERT,
+    BWO_DELETE
+};
+
+class ByteWindow : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit ByteWindow(QWidget *parent = nullptr);
+    ~ByteWindow();
+    void set(ByteWindowOption option, int offset, int size);
+    ByteWindowOption getOption();
+    int getOffset();
+    int getSize();
+
+private:
+    Ui::ByteWindow *ui;
+};
+#endif // BYTEWINDOW_H
diff --git a/bytewindow.ui b/bytewindow.ui
new file mode 100644
index 0000000..011207a
--- /dev/null
+++ b/bytewindow.ui
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ByteWindow</class>
+ <widget class="QDialog" name="ByteWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>300</width>
+    <height>200</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>300</width>
+    <height>200</height>
+   </size>
+  </property>
+  <property name="maximumSize">
+   <size>
+    <width>16777215</width>
+    <height>16777215</height>
+   </size>
+  </property>
+  <property name="windowTitle">
+   <string>Bytes Modification</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="2" column="1">
+    <widget class="QSpinBox" name="spinBoxSize">
+     <property name="minimum">
+      <number>1</number>
+     </property>
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="labelSize">
+     <property name="text">
+      <string>Size:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="1">
+    <widget class="QDialogButtonBox" name="buttonOk">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QSpinBox" name="spinBoxIndex">
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="labelIndex">
+     <property name="text">
+      <string>Index:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QRadioButton" name="radioButtonInsert">
+       <property name="text">
+        <string>Insert</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QRadioButton" name="radioButtonDelete">
+       <property name="text">
+        <string>Delete</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonOk</sender>
+   <signal>accepted()</signal>
+   <receiver>ByteWindow</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonOk</sender>
+   <signal>rejected()</signal>
+   <receiver>ByteWindow</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/mainwindow.cpp b/mainwindow.cpp
index a375d91..0060fe4 100644
--- a/mainwindow.cpp
+++ b/mainwindow.cpp
@@ -4,6 +4,8 @@
 #include <exception>
 #include <qfiledialog.h>
 
+#include <iostream>
+
 MainWindow::MainWindow(QWidget *parent)
     : QMainWindow(parent)
     , myHexEdit()
@@ -11,7 +13,93 @@ MainWindow::MainWindow(QWidget *parent)
     , ppp(nullptr)
 {
     ui->setupUi(this);
-    ui->gridLayout->addWidget(&myHexEdit);
+    ui->gridLayout->addWidget(&myHexEdit.editor);
+
+    const auto& enableButtons = [](MainWindow *mainwindow, bool enable, bool ui_edit_only = false) {
+        if (!ui_edit_only)
+            mainwindow->ui->actionSave->setEnabled(enable);
+        mainwindow->myHexEdit.prependBytes.setEnabled(enable);
+        mainwindow->myHexEdit.appendBytes.setEnabled(enable);
+        mainwindow->myHexEdit.deleteBytes.setEnabled(enable);
+        mainwindow->myHexEdit.deleteSelection.setEnabled(enable);
+    };
+    enableButtons(this, false);
+
+    myHexEdit.editor.setContextMenuPolicy(Qt::CustomContextMenu);
+    myHexEdit.prependBytes.setText("Prepend byte(s)..");
+    myHexEdit.appendBytes.setText("Append byte(s)..");
+    myHexEdit.deleteBytes.setText("Delete byte(s)..");
+    myHexEdit.deleteSelection.setText("Delete Selection");
+    myHexEdit.contextMenu.addAction(&myHexEdit.prependBytes);
+    myHexEdit.contextMenu.addAction(&myHexEdit.appendBytes);
+    myHexEdit.contextMenu.addAction(&myHexEdit.deleteBytes);
+    myHexEdit.contextMenu.addAction(&myHexEdit.deleteSelection);
+
+    connect(&myHexEdit.bytewindow, &QDialog::finished, this, [&](int result) {
+        if (result == 0)
+            return;
+
+        const auto option = myHexEdit.bytewindow.getOption();
+        const auto offset = myHexEdit.bytewindow.getOffset();
+        const auto size = myHexEdit.bytewindow.getSize();
+
+        const auto &selected = ui->tableWidget->selectedItems();
+        if (selected.empty())
+            return;
+        auto &rawPacket = ppp->getRawPacket(selected.last()->row());
+
+        switch (option) {
+        case ByteWindowOption::BWO_UNKNOWN:
+        case ByteWindowOption::BWO_INSERT: {
+            uint8_t *new_bytes = new uint8_t[size];
+            if (!new_bytes)
+                break;
+            memset(new_bytes, 0, size);
+            rawPacket.insertData(offset, new_bytes, size);
+            myHexEdit.editor.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+            delete[] new_bytes;
+            break;
+        }
+        case ByteWindowOption::BWO_DELETE:
+            rawPacket.removeData(offset, size);
+            myHexEdit.editor.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+            break;
+        }
+    });
+
+    connect(&myHexEdit.editor, &QAbstractScrollArea::customContextMenuRequested, this, [&](const QPoint& pos){
+        myHexEdit.deleteSelection.setEnabled(myHexEdit.editor.selectedData().size() != 0);
+
+        auto globalPos = myHexEdit.editor.mapToGlobal(pos);
+        auto selectedItem = myHexEdit.contextMenu.exec(globalPos);
+        const auto cursorPos = myHexEdit.editor.cursorPosition() / 2;
+        const auto& selectedLength = myHexEdit.editor.getSelectionEnd() - myHexEdit.editor.getSelectionBegin();
+        const auto& showByteWindow = [&](const ByteWindowOption& opt, int offset) {
+          myHexEdit.bytewindow.set(opt, offset, selectedLength > 0 ? selectedLength : 1);
+          myHexEdit.bytewindow.show();
+        };
+
+        if (selectedItem == &myHexEdit.prependBytes) {
+            showByteWindow(ByteWindowOption::BWO_INSERT, cursorPos);
+        } else if (selectedItem == &myHexEdit.appendBytes) {
+            showByteWindow(ByteWindowOption::BWO_INSERT, cursorPos + 1);
+        } else if (selectedItem == &myHexEdit.deleteBytes) {
+            showByteWindow(ByteWindowOption::BWO_DELETE, cursorPos);
+        } else if (selectedItem == &myHexEdit.deleteSelection) {
+            const auto &selected = ui->tableWidget->selectedItems();
+            if (selected.empty())
+                return;
+
+            auto &rawPacket = ppp->getRawPacket(selected.last()->row());
+            if (selectedLength == rawPacket.getRawDataLen())
+                return;
+
+            rawPacket.removeData(myHexEdit.editor.getSelectionBegin(), selectedLength);
+            myHexEdit.editor.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+        } else if (selectedItem) {
+            throw std::runtime_error("Unknown context menu " + selectedItem->text().toStdString());
+        }
+    });
 
     connect(ui->actionOpen, &QAction::triggered, this, [&](bool){
         QString fileName = QFileDialog::getOpenFileName(this, tr("Open PCAP File"), "", tr("PCAP Files (*.pcap)"));
@@ -20,6 +108,9 @@ MainWindow::MainWindow(QWidget *parent)
                 delete ppp;
                 ppp = nullptr;
             }
+            ui->lineEdit->clear();
+            ui->tableWidget->clear();
+            ui->tableWidget->setRowCount(0);
             ppp = new PcapPlusPlus(fileName.toStdString());
             if (ppp) emit processPcap();
         }
@@ -44,14 +135,16 @@ MainWindow::MainWindow(QWidget *parent)
     });
 
     connect(ui->actionSave, &QAction::triggered, this, [&](bool){
-        QString fileName = QFileDialog::getSaveFileName(this, tr("Save PCAP File"), "", tr("PCAP Files (*.pcap)"));
-        if (fileName.length() > 0) {
-            pcpp::PcapFileWriterDevice pcapWriter(fileName.toStdString(), pcpp::LINKTYPE_ETHERNET);
-            if (!pcapWriter.open())
-                throw std::runtime_error("Could not open file " + fileName.toStdString() + " for writing.");
-            {
-                for (auto rawPacket = ppp->rawPacketsBegin(); rawPacket < ppp->rawPacketsEnd(); rawPacket++) {
-                    pcapWriter.writePacket(*rawPacket);
+        if (ppp) {
+            QString fileName = QFileDialog::getSaveFileName(this, tr("Save PCAP File"), "", tr("PCAP Files (*.pcap)"));
+            if (fileName.length() > 0) {
+                pcpp::PcapFileWriterDevice pcapWriter(fileName.toStdString(), pcpp::LINKTYPE_ETHERNET);
+                if (!pcapWriter.open())
+                    throw std::runtime_error("Could not open file " + fileName.toStdString() + " for writing.");
+                {
+                    for (auto rawPacket = ppp->rawPacketsBegin(); rawPacket < ppp->rawPacketsEnd(); rawPacket++) {
+                        pcapWriter.writePacket(*rawPacket);
+                    }
                 }
             }
         }
@@ -128,15 +221,34 @@ MainWindow::MainWindow(QWidget *parent)
 
     connect(ui->tableWidget, &QTableWidget::cellPressed, this, [&] {
         const auto &selected = ui->tableWidget->selectedItems();
+        if (selected.empty())
+            return;
+
         const auto &rawPacket = ppp->getRawPacket(selected.last()->row());
-        myHexEdit.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+        myHexEdit.editor.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+    });
+
+    connect(ui->tableWidget, &QTableWidget::itemSelectionChanged, this, [&]() {
+        enableButtons(this, ui->tableWidget->selectedItems().size() > 0, true);
+        if (ui->tableWidget->selectedItems().size() == 0 && myHexEdit.editor.data().size() > 0)
+            myHexEdit.editor.setData(QByteArray());
     });
 
-    connect(&myHexEdit, &QHexEdit::dataChanged, this, [&] {
+    connect(&myHexEdit.editor, &QHexEdit::dataChanged, this, [&] {
         const auto &selected = ui->tableWidget->selectedItems();
+        if (selected.empty())
+            return;
+
         auto &rawPacket = ppp->getRawPacket(selected.last()->row());
-        const auto cursorPos = myHexEdit.cursorPosition() / 2;
-        const auto cursorData = myHexEdit.dataAt(cursorPos, 1);
+        const auto& cursorPos = myHexEdit.editor.cursorPosition() / 2;
+        auto cursorData = myHexEdit.editor.dataAt(cursorPos, 1);
+        if (myHexEdit.editor.cursorPosition() % 2 != 0 && myHexEdit.editor.cursorPosition() / 2 == myHexEdit.editor.data().size() - 1) {
+            const uint8_t new_byte = 0x00;
+            rawPacket.insertData(rawPacket.getRawDataLen(), &new_byte, sizeof(new_byte));
+            myHexEdit.editor.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(rawPacket.getRawData()), rawPacket.getRawDataLen()));
+            myHexEdit.editor.setCursorPosition(cursorPos * 2 + 1);
+        }
+
         rawPacket.removeData(cursorPos, 1);
         rawPacket.insertData(cursorPos, reinterpret_cast<const uint8_t *>(cursorData.data()), cursorData.size());
     });
diff --git a/mainwindow.h b/mainwindow.h
index 242ca65..74a6376 100644
--- a/mainwindow.h
+++ b/mainwindow.h
@@ -1,10 +1,12 @@
 #ifndef MAINWINDOW_H
 #define MAINWINDOW_H
 
+#include "bytewindow.h"
 #include "pcapplusplus.h"
 #include "qhexedit2/src/qhexedit.h"
 
 #include <QMainWindow>
+#include <QMenu>
 
 QT_BEGIN_NAMESPACE
 namespace Ui { class MainWindow; }
@@ -19,10 +21,19 @@ public:
     ~MainWindow();
 
 private:
-    QHexEdit myHexEdit;
+    struct {
+        QMenu contextMenu;
+        QAction prependBytes;
+        QAction appendBytes;
+        QAction deleteBytes;
+        QAction deleteSelection;
+        QHexEdit editor;
+        ByteWindow bytewindow;
+    } myHexEdit;
+
     time_t firstPacketTs;
-    Ui::MainWindow *ui;
     PcapPlusPlus *ppp;
+    Ui::MainWindow *ui;
 
 signals:
     void processPcap();
diff --git a/mainwindow.ui b/mainwindow.ui
index a0829c8..340b454 100644
--- a/mainwindow.ui
+++ b/mainwindow.ui
@@ -53,9 +53,15 @@ border-style: outset;
 border-width: 2px;
 border-color: beige;</string>
         </property>
+        <property name="selectionMode">
+         <enum>QAbstractItemView::ExtendedSelection</enum>
+        </property>
         <property name="selectionBehavior">
          <enum>QAbstractItemView::SelectRows</enum>
         </property>
+        <property name="cornerButtonEnabled">
+         <bool>true</bool>
+        </property>
         <property name="columnCount">
          <number>12</number>
         </property>
diff --git a/qhexedit2/src/qhexedit.h b/qhexedit2/src/qhexedit.h
index 1a5801a..f06aac5 100644
--- a/qhexedit2/src/qhexedit.h
+++ b/qhexedit2/src/qhexedit.h
@@ -365,7 +365,7 @@ public:
     QColor selectionColor();
     void setSelectionColor(const QColor &color);
 
-protected:
+public:
     // Handle events
     void keyPressEvent(QKeyEvent *event);
     void mouseMoveEvent(QMouseEvent * event);
@@ -373,7 +373,6 @@ protected:
     void paintEvent(QPaintEvent *event);
     void resizeEvent(QResizeEvent *);
     virtual bool focusNextPrevChild(bool next);
-private:
     // Handle selections
     void resetSelection(qint64 pos);            // set selectionStart and selectionEnd to pos
     void resetSelection();                      // set selectionEnd to selectionStart
-- 
cgit v1.2.3