/* packet_diagram.cpp * * Wireshark - Network traffic analyzer * By Gerald Combs * Copyright 1998 Gerald Combs * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "packet_diagram.h" #include "math.h" #include "epan/epan.h" #include "epan/epan_dissect.h" #include "wsutil/utf8_entities.h" #include "wireshark_application.h" #include "ui/qt/main_window.h" #include "ui/qt/utils/proto_node.h" #include "ui/qt/utils/variant_pointer.h" #include #include #include #if defined(QT_SVG_LIB) && 0 #include #include #include #endif #include // "rems" are root em widths, aka the regular font height, similar to rems in CSS. class DiagramLayout { public: DiagramLayout() : bits_per_row_(32), small_font_rems_(0.75), bit_width_rems_(1.0), padding_rems_(0.5), span_mark_offset_rems_(0.2), show_fields_(false) { setFont(wsApp->font()); } void setFont(QFont font) { regular_font_ = font; small_font_ = font; small_font_.setPointSize(regular_font_.pointSize() * small_font_rems_); QFontMetrics fm(regular_font_); root_em_ = fm.height(); } void setShowFields(bool show_fields = false) { show_fields_ = show_fields; } int bitsPerRow() const { return bits_per_row_; } const QFont regularFont() const { return regular_font_; } const QFont smallFont() const { return small_font_; } int bitWidth() const { return root_em_ * bit_width_rems_; } int lineHeight() const { return root_em_; } int hPadding() const { return root_em_ * padding_rems_; } int vPadding() const { return root_em_ * padding_rems_; } int spanMarkOffset() const { return root_em_ * span_mark_offset_rems_; } int rowHeight() const { int rows = show_fields_ ? 2 : 1; return ((lineHeight() * rows) + (vPadding() * 2)); } bool showFields() const { return show_fields_; } private: int bits_per_row_; double small_font_rems_; double bit_width_rems_; double padding_rems_; double span_mark_offset_rems_; // XXX Make this padding_rems_ / 2 instead? bool show_fields_; QFont regular_font_; QFont small_font_; int root_em_; }; class FieldInformationGraphicsItem : public QGraphicsPolygonItem { public: FieldInformationGraphicsItem(field_info *fi, int start_bit, int fi_length, const DiagramLayout *layout, QGraphicsItem *parent = nullptr) : QGraphicsPolygonItem(QPolygonF(), parent), finfo_(new FieldInformation(fi)), start_bit_(start_bit), layout_(layout), collapsed_len_(fi_length), collapsed_row_(-1) { Q_ASSERT(layout_); for (int idx = 0; idx < NumSpanMarks; idx++) { span_marks_[idx] = new QGraphicsLineItem(this); span_marks_[idx]->hide(); } int bits_per_row = layout_->bitsPerRow(); int row1_start = start_bit_ % bits_per_row; int bits_remain = fi_length; int row1_bits = bits_remain; if (bits_remain + row1_start > bits_per_row) { row1_bits = bits_per_row - row1_start; bits_remain -= row1_bits; if (row1_start == 0 && bits_remain > bits_per_row) { // Collapse first row bits_remain %= bits_per_row; collapsed_row_ = 0; } } else { bits_remain = 0; } int row2_bits = bits_remain; if (bits_remain > bits_per_row) { row2_bits = bits_per_row; bits_remain -= bits_per_row; if (bits_remain > bits_per_row) { // Collapse second row bits_remain %= bits_per_row; collapsed_row_ = 1; } } else { bits_remain = 0; } int row3_bits = bits_remain; collapsed_len_ = row1_bits + row2_bits + row3_bits; QRectF rr1, rr2, rr3; QRectF row_rect = QRectF(row1_start, 0, row1_bits, 1); unit_shape_ = QPolygonF(row_rect); rr1 = row_rect; unit_tr_ = row_rect; if (row2_bits > 0) { row_rect = QRectF(0, 1, row2_bits, 1); unit_shape_ = unit_shape_.united(QPolygonF(row_rect)); rr2 = row_rect; if (row2_bits > row1_bits) { unit_tr_ = row_rect; } if (row3_bits > 0) { row_rect = QRectF(0, 2, row3_bits, 1); unit_shape_ = unit_shape_.united(QPolygonF(row_rect)); rr3 = row_rect; } QPainterPath pp; pp.addPolygon(unit_shape_); unit_shape_ = pp.simplified().toFillPolygon(); } updateLayout(); if (finfo_->isValid()) { setToolTip(QString("%1 (%2) = %3") .arg(finfo_->headerInfo().name) .arg(finfo_->headerInfo().abbreviation) .arg(finfo_->toString())); setData(Qt::UserRole, VariantPointer::asQVariant(finfo_->fieldInfo())); } else { setToolTip(QObject::tr("Gap in dissection")); } } int collapsedLength() { return collapsed_len_; } void setPos(qreal x, qreal y) { QGraphicsPolygonItem::setPos(x, y); updateLayout(); } int maxLeftY() { qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_; QPointF pt = mapToParent(QPointF(0, ceil(rel_len / layout_->bitsPerRow()) * layout_->rowHeight())); return pt.y(); } int maxRightY() { qreal rel_len = (start_bit_ % layout_->bitsPerRow()) + collapsed_len_; QPointF pt = mapToParent(QPointF(0, floor(rel_len / layout_->bitsPerRow()) * layout_->rowHeight())); return pt.y(); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { painter->setPen(Qt::NoPen); painter->save(); if (!finfo_->isValid()) { QBrush brush = QBrush(option->palette.text().color(), Qt::BDiagPattern); painter->setBrush(brush); } else if (isSelected()) { painter->setBrush(option->palette.highlight().color()); } painter->drawPolygon(polygon()); painter->restore(); // Lower and inner right borders painter->setPen(option->palette.text().color()); QPolygonF shape = polygon(); for (int idx = 1; idx < unit_shape_.size(); idx++) { QPointF u_start = unit_shape_[idx - 1]; QPointF u_end = unit_shape_[idx]; QPointF start, end; bool draw_line = false; if (u_start.y() > 0 && u_start.y() == u_end.y()) { draw_line = true; } else if (u_start.x() > 0 && u_start.x() < layout_->bitsPerRow() && u_start.x() == u_end.x()) { draw_line = true; } if (draw_line) { start = shape[idx - 1]; end = shape[idx]; painter->drawLine(start, end); } } if (!finfo_->isValid()) { return; } // Field label(s) QString label = finfo_->headerInfo().name; paintLabel(painter, label, scaled_tr_); if (layout_->showFields()) { label = finfo_->toString(); paintLabel(painter, label, scaled_tr_.adjusted(0, scaled_tr_.height(), 0, scaled_tr_.height())); } } private: enum SpanMark { TopLeft, BottomLeft, TopRight, BottomRight, NumSpanMarks }; FieldInformation *finfo_; int start_bit_; const DiagramLayout *layout_; int collapsed_len_; int collapsed_row_; QPolygonF unit_shape_; QRectF unit_tr_; QRectF scaled_tr_; QGraphicsLineItem *span_marks_[NumSpanMarks]; void updateLayout() { QTransform xform; xform.scale(layout_->bitWidth(), layout_->rowHeight()); setPolygon(xform.map(unit_shape_)); scaled_tr_ = xform.mapRect(unit_tr_); scaled_tr_.adjust(layout_->hPadding(), layout_->vPadding(), -layout_->hPadding(), -layout_->vPadding()); scaled_tr_.setHeight(layout_->lineHeight()); // Collapsed / span marks for (int idx = 0; idx < NumSpanMarks; idx++) { span_marks_[idx]->hide(); } if (collapsed_row_ >= 0) { QRectF bounding_rect = polygon().boundingRect(); qreal center_y = bounding_rect.top() + (layout_->rowHeight() * collapsed_row_) + (layout_->rowHeight() / 2); qreal mark_w = layout_->bitWidth() / 3; // Each mark side to center QLineF span_l = QLineF(-mark_w, mark_w / 2, mark_w, -mark_w / 2); for (int idx = 0; idx < NumSpanMarks; idx++) { QPointF center; switch (idx) { case TopLeft: center = QPointF(bounding_rect.left(), center_y - layout_->spanMarkOffset()); break; case BottomLeft: center = QPointF(bounding_rect.left(), center_y + layout_->spanMarkOffset()); break; case TopRight: center = QPointF(bounding_rect.right(), center_y - layout_->spanMarkOffset()); break; case BottomRight: center = QPointF(bounding_rect.right(), center_y + layout_->spanMarkOffset()); break; } span_marks_[idx]->setLine(span_l.translated(center)); span_marks_[idx]->setZValue(zValue() - 0.1); span_marks_[idx]->show(); } } } void paintLabel(QPainter *painter, QString label, QRectF label_rect) { QFontMetrics fm = QFontMetrics(layout_->regularFont()); painter->setFont(layout_->regularFont()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) int label_w = fm.horizontalAdvance(label); #else int label_w = fm.width(label); #endif if (label_w > label_rect.width()) { painter->setFont(layout_->smallFont()); fm = QFontMetrics(layout_->smallFont()); #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)) label_w = fm.horizontalAdvance(label); #else label_w = fm.width(label); #endif if (label_w > label_rect.width()) { // XXX Use parent+ItemClipsChildrenToShape or setScale instead? label = fm.elidedText(label, Qt::ElideRight, label_rect.width()); } } painter->drawText(label_rect, Qt::AlignCenter, label); } }; PacketDiagram::PacketDiagram(QWidget *parent) : QGraphicsView(new QGraphicsScene(), parent), layout_(new DiagramLayout), cap_file_(nullptr), root_node_(nullptr), selected_field_(nullptr), y_pos_(0) { setAccessibleName(tr("Packet diagram")); setRenderHint(QPainter::Antialiasing); // XXX Move to setMonospaceFont similar to ProtoTree layout_->setFont(font()); connect(wsApp, &WiresharkApplication::appInitialized, this, &PacketDiagram::connectToMainWindow); QGraphicsScene *this_scene = scene(); connect(this_scene, &QGraphicsScene::selectionChanged, this, &PacketDiagram::sceneSelectionChanged); connect(wsApp, &WiresharkApplication::zoomRegularFont, this, &PacketDiagram::setFont); } PacketDiagram::~PacketDiagram() { delete layout_; } void PacketDiagram::setRootNode(proto_node *root_node) { scene()->clear(); root_node_ = root_node; selected_field_ = nullptr; y_pos_ = 0; ProtoNode parent_node(root_node_); if (!parent_node.isValid()) { return; } ProtoNode::ChildIterator kids = parent_node.children(); while (kids.element().isValid()) { proto_node *tl_node = kids.element().protoNode(); kids.next(); // Exclude all ("Frame") and nothing if (tl_node->finfo->start == 0 && tl_node->finfo->length == (int) tvb_captured_length(cap_file_->edt->tvb)) { continue; } if (tl_node->finfo->length < 1) { continue; } addDiagram(tl_node); } } void PacketDiagram::clear() { setRootNode(nullptr); } void PacketDiagram::setCaptureFile(capture_file *cf) { // For use by the main view, set the capture file which will later have a // dissection (EDT) ready. // The packet dialog sets a fixed EDT context and MUST NOT use this. cap_file_ = cf; } void PacketDiagram::setFont(const QFont &font) { layout_->setFont(font); setRootNode(root_node_); } void PacketDiagram::selectedFieldChanged(FieldInformation *finfo) { setSelectedField(finfo ? finfo->fieldInfo() : nullptr); } void PacketDiagram::selectedFrameChanged(QList frames) { if (frames.count() == 1 && cap_file_ && cap_file_->edt && cap_file_->edt->tree) { setRootNode(cap_file_->edt->tree); } else { // Clear the proto tree contents as they have become invalid. setRootNode(nullptr); } } void PacketDiagram::contextMenuEvent(QContextMenuEvent *event) { if (!event) { return; } QAction *action; QMenu ctx_menu(this); action = ctx_menu.addAction(tr("Show Field Values")); action->setCheckable(true); action->setChecked(layout_->showFields()); connect(action, &QAction::toggled, this, &PacketDiagram::showFieldsToggled); ctx_menu.addSeparator(); action = ctx_menu.addAction(tr("Save Diagram As" UTF8_HORIZONTAL_ELLIPSIS)); connect(action, &QAction::triggered, this, &PacketDiagram::saveAsTriggered); action = ctx_menu.addAction(tr("Copy as Raster Image")); connect(action, &QAction::triggered, this, &PacketDiagram::copyAsRasterTriggered); #if defined(QT_SVG_LIB) && !defined(Q_OS_MAC) action = ctx_menu.addAction(tr(UTF8_HORIZONTAL_ELLIPSIS "as SVG")); connect(action, &QAction::triggered, this, &PacketDiagram::copyAsSvgTriggered); #endif ctx_menu.exec(event->globalPos()); } void PacketDiagram::connectToMainWindow() { MainWindow *main_window = qobject_cast(wsApp->mainWindow()); if (!main_window) { return; } connect(main_window, &MainWindow::setCaptureFile, this, &PacketDiagram::setCaptureFile); connect(main_window, &MainWindow::fieldSelected, this, &PacketDiagram::selectedFieldChanged); connect(main_window, &MainWindow::framesSelected, this, &PacketDiagram::selectedFrameChanged); connect(this, &PacketDiagram::fieldSelected, main_window, &MainWindow::fieldSelected); } void PacketDiagram::sceneSelectionChanged() { field_info *sel_fi = nullptr; if (! scene()->selectedItems().isEmpty()) { sel_fi = VariantPointer::asPtr(scene()->selectedItems().first()->data(Qt::UserRole)); } if (sel_fi) { FieldInformation finfo(sel_fi, this); emit fieldSelected(&finfo); } else { emit fieldSelected(nullptr); } } struct WireItem { proto_item *item; int start_bit; int length; }; void PacketDiagram::addDiagram(proto_node *tl_node) { QGraphicsItem *item; QGraphicsSimpleTextItem *t_item; int bits_per_row = layout_->bitsPerRow(); int bit_width = layout_->bitWidth(); int diag_w = bit_width * layout_->bitsPerRow(); qreal x = layout_->hPadding(); // Title t_item = scene()->addSimpleText(tl_node->finfo->hfinfo->name); t_item->setFont(layout_->regularFont()); t_item->setPos(0, y_pos_); y_pos_ += layout_->lineHeight() + (bit_width / 4); int border_top = y_pos_; // Bit scale + tick marks QList tick_nums; for (int tn = 0 ; tn < layout_->bitsPerRow(); tn += 16) { tick_nums << tn << tn + 15; } qreal y_bottom = y_pos_ + bit_width; QGraphicsItem *tl_item = scene()->addLine(x, y_bottom, x + diag_w, y_bottom); for (int tick_n = 0; tick_n < bits_per_row; tick_n++) { x = layout_->hPadding() + (tick_n * bit_width); qreal y_top = y_pos_ + (tick_n % 8 == 0 ? 0 : bit_width / 2); if (tick_n > 0) { scene()->addLine(x, y_top, x, y_bottom); } if (tick_nums.contains(tick_n)) { t_item = scene()->addSimpleText(QString::number(tick_n)); t_item->setFont(layout_->smallFont()); t_item->setPos(x + ((bit_width - t_item->boundingRect().width()) / 2.0) + 2, y_pos_); } } y_pos_ = y_bottom; x = layout_->hPadding(); // Top-level fields int last_start_bit = -1; int max_l_y = y_bottom; QListwire_items; for (proto_item *cur_item = tl_node->first_child; cur_item; cur_item = cur_item->next) { if (proto_item_is_generated(cur_item) || proto_item_is_hidden(cur_item)) { continue; } field_info *fi = cur_item->finfo; int start_bit = ((fi->start - tl_node->finfo->start) * 8) + FI_GET_BITS_OFFSET(fi); int length = FI_GET_BITS_SIZE(fi) ? FI_GET_BITS_SIZE(fi) : fi->length * 8; if (start_bit <= last_start_bit || length <= 0) { qDebug() << "Skipping pass 1" << fi->hfinfo->abbrev << start_bit << last_start_bit << length; continue; } last_start_bit = start_bit; WireItem wire_item = { cur_item, start_bit, length }; wire_items << wire_item; } qreal z_value = tl_item->zValue(); for (int idx = 0; idx < wire_items.size(); idx++) { WireItem *wire_item = &wire_items[idx]; field_info *fi = wire_item->item->finfo; if (idx < wire_items.size() - 1) { WireItem *next_item = &wire_items[idx + 1]; if (wire_item->start_bit + wire_item->length > next_item->start_bit) { wire_item->length = next_item->start_bit - wire_item->start_bit; qDebug() << "Resized pass 2" << fi->hfinfo->abbrev << wire_item->start_bit << wire_item->length << next_item->start_bit; if (wire_item->length <= 0) { qDebug() << "Skipping pass 2" << fi->hfinfo->abbrev << wire_item->start_bit << wire_item->length; continue; } } if (next_item->start_bit > wire_item->start_bit + wire_item->length) { int gap_start_bit = wire_item->start_bit + wire_item->length; int gap_len = next_item->start_bit - gap_start_bit; int y_off = (gap_start_bit / bits_per_row) * layout_->rowHeight(); // Stack each item behind the previous one. z_value -= .01; FieldInformationGraphicsItem *gap_item = new FieldInformationGraphicsItem(nullptr, gap_start_bit, gap_len, layout_); gap_item->setPos(x, y_bottom + y_off); gap_item->setZValue(z_value); scene()->addItem(gap_item); } } int y_off = (wire_item->start_bit / bits_per_row) * layout_->rowHeight(); // Stack each item behind the previous one. z_value -= .01; FieldInformationGraphicsItem *fi_item = new FieldInformationGraphicsItem(fi, wire_item->start_bit, wire_item->length, layout_); fi_item->setPos(x, y_bottom + y_off); fi_item->setFlag(QGraphicsItem::ItemIsSelectable); fi_item->setAcceptedMouseButtons(Qt::LeftButton); fi_item->setZValue(z_value); scene()->addItem(fi_item); y_pos_ = fi_item->maxRightY(); max_l_y = fi_item->maxLeftY(); } // Left & right borders scene()->addLine(x, border_top, x, max_l_y); scene()->addLine(x + diag_w, border_top, x + diag_w, y_pos_); // Inter-diagram margin y_pos_ = max_l_y + bit_width; // Set the proper color. Needed for dark mode on macOS + Qt 5.15.0 at least, possibly other cases. foreach (item, scene()->items()) { QGraphicsSimpleTextItem *t_item = qgraphicsitem_cast(item); if (t_item) { t_item->setBrush(palette().text().color()); } QGraphicsLineItem *l_item = qgraphicsitem_cast(item); if (l_item) { l_item->setPen(palette().text().color()); } } } void PacketDiagram::setSelectedField(field_info *fi) { QSignalBlocker blocker(this); FieldInformationGraphicsItem *fi_item; foreach (QGraphicsItem *item, scene()->items()) { if (item->isSelected()) { item->setSelected(false); fi_item = qgraphicsitem_cast(item); } if (fi && VariantPointer::asPtr(item->data(Qt::UserRole)) == fi) { fi_item = qgraphicsitem_cast(item); if (fi_item) { fi_item->setSelected(true); } } } } QImage PacketDiagram::exportToImage() { // Create a hi-res 2x scaled image. int scale = 2; QRect rr = QRect(0, 0, sceneRect().size().width() * scale, sceneRect().size().height() * scale); QImage raster_diagram = QImage(rr.size(), QImage::Format_ARGB32); QPainter raster_painter(&raster_diagram); raster_painter.setRenderHint(QPainter::Antialiasing); raster_painter.fillRect(rr, palette().base().color()); scene()->render(&raster_painter); raster_painter.end(); return raster_diagram; } #if defined(QT_SVG_LIB) && 0 QByteArray PacketDiagram::exportToSvg() { QRect sr = QRect(0, 0, sceneRect().size().width(), sceneRect().size().height()); QBuffer svg_buf; QSvgGenerator svg_diagram; svg_diagram.setSize(sr.size()); svg_diagram.setViewBox(sr); svg_diagram.setOutputDevice(&svg_buf); QPainter svg_painter(&svg_diagram); svg_painter.fillRect(sr, palette().base().color()); scene()->render(&svg_painter); svg_painter.end(); return svg_buf.buffer(); } #endif void PacketDiagram::showFieldsToggled(bool checked) { layout_->setShowFields(checked); setRootNode(root_node_); } // XXX - We have similar code in tcp_stream_dialog and io_graph_dialog. Should this be a common routine? void PacketDiagram::saveAsTriggered() { QString file_name, extension; QDir path(wsApp->lastOpenDir()); QString png_filter = tr("Portable Network Graphics (*.png)"); QString bmp_filter = tr("Windows Bitmap (*.bmp)"); // Gaze upon my beautiful graph with lossy artifacts! QString jpeg_filter = tr("JPEG File Interchange Format (*.jpeg *.jpg)"); QStringList fl = QStringList() << png_filter << bmp_filter << jpeg_filter; #if defined(QT_SVG_LIB) && 0 QString svg_filter = tr("Scalable Vector Graphics (*.svg)"); fl << svg_filter; #endif QString filter = fl.join(";;"); file_name = WiresharkFileDialog::getSaveFileName(this, wsApp->windowTitleString(tr("Save Graph As" UTF8_HORIZONTAL_ELLIPSIS)), path.canonicalPath(), filter, &extension); if (file_name.length() > 0) { bool save_ok = false; if (extension.compare(png_filter) == 0) { QImage raster_diagram = exportToImage(); save_ok = raster_diagram.save(file_name, "PNG"); } else if (extension.compare(bmp_filter) == 0) { QImage raster_diagram = exportToImage(); save_ok = raster_diagram.save(file_name, "BMP"); } else if (extension.compare(jpeg_filter) == 0) { QImage raster_diagram = exportToImage(); save_ok = raster_diagram.save(file_name, "JPG"); } #if defined(QT_SVG_LIB) && 0 else if (extension.compare(svg_filter) == 0) { QByteArray svg_diagram = exportToSvg(); QFile file(file_name); if (file.open(QIODevice::WriteOnly)) { save_ok = file.write(svg_diagram) > 0; file.close(); } } #endif // else error dialog? if (save_ok) { path = QDir(file_name); wsApp->setLastOpenDir(path.canonicalPath().toUtf8().constData()); } } } void PacketDiagram::copyAsRasterTriggered() { QImage raster_diagram = exportToImage(); wsApp->clipboard()->setImage(raster_diagram); } #if defined(QT_SVG_LIB) && !defined(Q_OS_MAC) && 0 void PacketDiagram::copyAsSvgTriggered() { QByteArray svg_ba = exportToSvg(); // XXX It looks like we have to use/subclass QMacPasteboardMime in // order for this to work on macOS. // It might be easier to just do "Save As" instead. QMimeData *md = new QMimeData(); md->setData("image/svg+xml", svg_buf); wsApp->clipboard()->setMimeData(md); } #endif