aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-20 22:21:55 +0200
committersaturneric <[email protected]>2026-09-20 22:21:55 +0200
commit8cacac12288d22a7c81058b1b765bd24e56612f8 (patch)
tree72fc6c0fed3b5c216d43e77fd1cb536278211413
parentbuild(cmake): organize module output directories (diff)
downloadmodules-8cacac12288d22a7c81058b1b765bd24e56612f8.tar.gz
modules-8cacac12288d22a7c81058b1b765bd24e56612f8.zip
feat(pgp): add OpenPGP structure inspector
- add sandbox-compatible module for inspecting packet structure - render armor, cleartext, packet fields, errors, and searchable trees - parse inspector JSON with nested packet support and summaries - add model tests covering parsing, structure detection, and formatting - add module metadata, build integration, and translation catalogs
-rw-r--r--src/CMakeLists.txt4
-rw-r--r--src/m_pgp_inspect/CMakeLists.txt41
-rw-r--r--src/m_pgp_inspect/PGPInspectDialog.cpp237
-rw-r--r--src/m_pgp_inspect/PGPInspectDialog.h88
-rw-r--r--src/m_pgp_inspect/PGPInspectModel.cpp191
-rw-r--r--src/m_pgp_inspect/PGPInspectModel.h141
-rw-r--r--src/m_pgp_inspect/PGPInspectModule.cpp132
-rw-r--r--src/m_pgp_inspect/module.json15
-rw-r--r--src/m_pgp_inspect/test/CMakeLists.txt53
-rw-r--r--src/m_pgp_inspect/test/PGPInspectModelTest.cpp298
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts105
-rw-r--r--src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts105
19 files changed, 2145 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4f6f497..e1e9b4b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,6 +27,10 @@
add_subdirectory(m_key_server_sync)
add_subdirectory(m_email)
+# No network and no GnuPG: an inspector reads bytes and nothing else, so it is
+# fine in the sandboxed build too.
+add_subdirectory(m_pgp_inspect)
+
# modules that are not needed in sandboxed environment
if(NOT GPGFRONTEND_XCODE_ENABLE_SANDBOX)
add_subdirectory(m_ver_check)
diff --git a/src/m_pgp_inspect/CMakeLists.txt b/src/m_pgp_inspect/CMakeLists.txt
new file mode 100644
index 0000000..705ac23
--- /dev/null
+++ b/src/m_pgp_inspect/CMakeLists.txt
@@ -0,0 +1,41 @@
+# Copyright (C) 2021-2024 Saturneric <[email protected]>
+#
+# This file is part of GpgFrontend.
+#
+# GpgFrontend 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.
+#
+# GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+#
+# The initial version of the source code is inherited from
+# the gpg4usb project, which is under GPL-3.0-or-later.
+#
+# All the source code of GpgFrontend was modified and released by
+# Saturneric <[email protected]> starting on May 12, 2021.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Everything about this module -- id, version, name, description, author,
+# capabilities and translation context -- lives in module.json beside this file.
+#
+# SOURCES is spelled out rather than left to aux_source_directory: the test
+# directory below compiles PGPInspectModel.cpp on its own, and a stray file
+# picked up by a glob would quietly break that split.
+gf_add_module(
+ NAME pgp_inspect
+ QT Core Widgets
+ SOURCES PGPInspectModule.cpp
+ PGPInspectModel.cpp
+ PGPInspectDialog.cpp)
+
+if(GPGFRONTEND_MODULES_BUILD_TESTS)
+ add_subdirectory(test)
+endif()
diff --git a/src/m_pgp_inspect/PGPInspectDialog.cpp b/src/m_pgp_inspect/PGPInspectDialog.cpp
new file mode 100644
index 0000000..e8c58cf
--- /dev/null
+++ b/src/m_pgp_inspect/PGPInspectDialog.cpp
@@ -0,0 +1,237 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "PGPInspectDialog.h"
+
+#include <GFModule.h>
+#include <GFSDKPgp.h>
+#include <GFSDKUI.h>
+
+#include <QHeaderView>
+#include <QLabel>
+#include <QLineEdit>
+#include <QTreeWidget>
+#include <QVBoxLayout>
+
+namespace {
+
+/// Which column holds what. Packet rows use the first only; a field row puts
+/// its label there and its value beside it.
+constexpr int kLabelColumn = 0;
+constexpr int kValueColumn = 1;
+
+auto HumanSize(qint64 bytes) -> QString { return UDUP(GFUIHumanSize(bytes)); }
+
+} // namespace
+
+auto PGPInspectCurrentTabBytes() -> QByteArray {
+ auto* content = GFUITakeCurrentEditorContent();
+ if (content == nullptr) return {};
+
+ const auto* data = static_cast<const char*>(GFBufferData(content));
+ const auto size = static_cast<qsizetype>(GFBufferSize(content));
+ auto bytes = QByteArray(data == nullptr ? "" : data, size);
+ GFBufferRelease(content);
+ return bytes;
+}
+
+auto PGPInspectBytes(const QByteArray& data) -> PGPInspectDocument {
+ PGPInspectDocument document;
+ if (data.isEmpty()) return document;
+
+ auto* buffer =
+ GFBufferNewFromBytes(data.constData(), static_cast<size_t>(data.size()));
+ if (buffer == nullptr) return document;
+
+ char* json = nullptr;
+ const auto status = GFPgpInspectData(buffer, &json);
+ GFBufferRelease(buffer);
+
+ if (status != 0 || json == nullptr) return document;
+ return ParsePGPInspectDocument(UDUP(json).toUtf8());
+}
+
+PGPInspectDialog::PGPInspectDialog(const PGPInspectDocument& document,
+ qint64 size, QWidget* parent)
+ : QDialog(parent), document_(document), size_(size) {
+ setWindowTitle(QCoreApplication::translate("GTrC", "OpenPGP Structure"));
+ setAttribute(Qt::WA_DeleteOnClose);
+ resize(820, 620);
+ create_widgets();
+ render_document();
+}
+
+void PGPInspectDialog::create_widgets() {
+ search_ = new QLineEdit(this);
+ search_->setClearButtonEnabled(true);
+ search_->setPlaceholderText(
+ QCoreApplication::translate("GTrC", "Search packets, fields and values"));
+ connect(search_, &QLineEdit::textChanged, this,
+ &PGPInspectDialog::slot_filter_changed);
+
+ tree_ = new QTreeWidget(this);
+ tree_->setColumnCount(2);
+ tree_->setHeaderLabels({QCoreApplication::translate("GTrC", "Structure"),
+ QCoreApplication::translate("GTrC", "Value")});
+ tree_->header()->setSectionResizeMode(kLabelColumn,
+ QHeaderView::ResizeToContents);
+ tree_->header()->setSectionResizeMode(kValueColumn, QHeaderView::Stretch);
+ tree_->setUniformRowHeights(true);
+
+ summary_label_ = new QLabel(this);
+ summary_label_->setWordWrap(true);
+
+ auto* layout = new QVBoxLayout();
+ layout->addWidget(search_);
+ layout->addWidget(tree_);
+ layout->addWidget(summary_label_);
+ setLayout(layout);
+}
+
+void PGPInspectDialog::render_document() {
+ tree_->clear();
+
+ if (!document_.valid) {
+ summary_label_->setText(
+ document_.parse_error.isEmpty()
+ ? QCoreApplication::translate(
+ "GTrC", "The current tab holds no OpenPGP data.")
+ : document_.parse_error);
+ return;
+ }
+
+ int index = 1;
+ for (const auto& block : document_.blocks) {
+ auto* item = new QTreeWidgetItem(tree_);
+ item->setText(kLabelColumn, PGPInspectBlockSummary(block, index++));
+
+ // The block's own facts are rows like any other, so the search box reaches
+ // the armor headers and the CRC verdict too.
+ QVector<PGPInspectField> facts;
+ if (block.has_armor) {
+ facts.push_back({QCoreApplication::translate("GTrC", "Armor Type"),
+ block.armor.block_type});
+ facts.push_back(
+ {QCoreApplication::translate("GTrC", "CRC24"), block.armor.crc24});
+ facts += block.armor.headers;
+ }
+ if (block.has_cleartext) {
+ facts.push_back({QCoreApplication::translate("GTrC", "Signed Text"),
+ HumanSize(block.cleartext_text_size)});
+ facts += block.cleartext_headers;
+ }
+ if (!block.error.isEmpty()) {
+ facts.push_back(
+ {QCoreApplication::translate("GTrC", "Problem"), block.error});
+ }
+ add_fields(item, facts);
+
+ if (block.has_armor && block.armor.crc24 == QLatin1String("mismatch")) {
+ item->setForeground(kLabelColumn, QBrush(QColor(GFUIDangerColor(this))));
+ } else if (!block.error.isEmpty()) {
+ item->setForeground(kLabelColumn, QBrush(QColor(GFUIWarningColor(this))));
+ }
+
+ add_packets(item, block.packets);
+ }
+
+ tree_->expandAll();
+
+ // The count goes through a local: a function call inside a translate()
+ // %n argument makes lupdate silently skip EVERY string in the file.
+ const int packet_count = document_.PacketCount();
+ auto summary = QCoreApplication::translate("GTrC", "%1, %2, %n packet(s)",
+ nullptr, packet_count)
+ .arg(document_.format, HumanSize(size_));
+ if (!document_.errors.isEmpty()) {
+ summary +=
+ QLatin1String(" - ") + document_.errors.join(QLatin1String("; "));
+ }
+ summary_label_->setText(summary);
+}
+
+void PGPInspectDialog::add_packets(QTreeWidgetItem* parent,
+ const QVector<PGPInspectPacket>& packets) {
+ int index = 1;
+ for (const auto& packet : packets) {
+ auto* item = new QTreeWidgetItem(parent);
+ item->setText(kLabelColumn, PGPInspectPacketSummary(packet, index++));
+
+ if (packet.Malformed()) {
+ item->setForeground(kLabelColumn, QBrush(QColor(GFUIWarningColor(this))));
+ }
+
+ auto fields = packet.fields;
+ if (packet.Malformed()) {
+ fields.push_back(
+ {QCoreApplication::translate("GTrC", "Problem"), packet.error});
+ }
+ add_fields(item, fields);
+
+ add_packets(item, packet.children);
+ }
+}
+
+void PGPInspectDialog::add_fields(QTreeWidgetItem* parent,
+ const QVector<PGPInspectField>& fields) {
+ for (const auto& field : fields) {
+ auto* row = new QTreeWidgetItem(parent);
+ row->setText(kLabelColumn, field.label);
+ row->setText(kValueColumn, field.value);
+ row->setForeground(kLabelColumn, QBrush(QColor(GFUIMutedTextColor(this))));
+ }
+}
+
+void PGPInspectDialog::slot_filter_changed(const QString& needle) {
+ for (int i = 0; i < tree_->topLevelItemCount(); ++i) {
+ PGPInspectFilterItem(tree_->topLevelItem(i), needle, false);
+ }
+ if (!needle.isEmpty()) tree_->expandAll();
+}
+
+auto PGPInspectFilterItem(QTreeWidgetItem* item, const QString& needle,
+ bool ancestor_matched) -> bool {
+ if (item == nullptr) return false;
+
+ const auto matched = needle.isEmpty() ||
+ item->text(0).contains(needle, Qt::CaseInsensitive) ||
+ item->text(1).contains(needle, Qt::CaseInsensitive);
+
+ // A matching packet keeps its whole subtree: the reader searched for the
+ // packet, so hiding the fields that describe it would answer the wrong
+ // question.
+ auto descendant_matched = false;
+ for (int i = 0; i < item->childCount(); ++i) {
+ descendant_matched |= PGPInspectFilterItem(item->child(i), needle,
+ ancestor_matched || matched);
+ }
+
+ const auto visible = matched || descendant_matched || ancestor_matched;
+ item->setHidden(!visible);
+ return visible;
+}
diff --git a/src/m_pgp_inspect/PGPInspectDialog.h b/src/m_pgp_inspect/PGPInspectDialog.h
new file mode 100644
index 0000000..d84b7c9
--- /dev/null
+++ b/src/m_pgp_inspect/PGPInspectDialog.h
@@ -0,0 +1,88 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include <QByteArray>
+#include <QDialog>
+
+#include "PGPInspectModel.h"
+
+class QLabel;
+class QLineEdit;
+class QTreeWidget;
+class QTreeWidgetItem;
+
+/// The exact octets of the current editor tab, or empty when there is no tab.
+auto PGPInspectCurrentTabBytes() -> QByteArray;
+
+/// Run the inspector over @p data. Never throws; see PGPInspectDocument::valid.
+auto PGPInspectBytes(const QByteArray& data) -> PGPInspectDocument;
+
+/**
+ * @brief Shows what the OpenPGP data in the current tab is made of.
+ *
+ * A dumb view, and a deliberately bare one: it is handed a document and
+ * renders it. There is nothing to press -- the content comes from the tab the
+ * user was already looking at, so a dialog that asked them to fetch it again
+ * would be asking a question they have already answered. The only control is
+ * the search box, which is there because a certificate's packet tree is long.
+ *
+ * Every rule about what the bytes mean lives in PGPInspectModel.h and, below
+ * that, in the Rust packet walk.
+ */
+class PGPInspectDialog : public QDialog {
+ Q_OBJECT
+
+ public:
+ PGPInspectDialog(const PGPInspectDocument& document, qint64 size,
+ QWidget* parent = nullptr);
+
+ private slots:
+ void slot_filter_changed(const QString& needle);
+
+ private:
+ void create_widgets();
+ void render_document();
+ void add_packets(QTreeWidgetItem* parent,
+ const QVector<PGPInspectPacket>& packets);
+ void add_fields(QTreeWidgetItem* parent,
+ const QVector<PGPInspectField>& fields);
+
+ PGPInspectDocument document_;
+ qint64 size_ = 0;
+
+ QLineEdit* search_{};
+ QTreeWidget* tree_{};
+ QLabel* summary_label_{};
+};
+
+/// Show a row when it matches, when an ancestor matched, or when a descendant
+/// does. Returns whether @p item ended up visible.
+auto PGPInspectFilterItem(QTreeWidgetItem* item, const QString& needle,
+ bool ancestor_matched) -> bool;
diff --git a/src/m_pgp_inspect/PGPInspectModel.cpp b/src/m_pgp_inspect/PGPInspectModel.cpp
new file mode 100644
index 0000000..874b349
--- /dev/null
+++ b/src/m_pgp_inspect/PGPInspectModel.cpp
@@ -0,0 +1,191 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "PGPInspectModel.h"
+
+#include <QCoreApplication>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+
+namespace {
+
+auto ParseFields(const QJsonValue& value) -> QVector<PGPInspectField> {
+ QVector<PGPInspectField> fields;
+ for (const auto& entry : value.toArray()) {
+ const auto object = entry.toObject();
+ // A row with neither half says nothing; dropping it keeps the detail
+ // table from growing blank lines out of a malformed document.
+ if (!object.contains("label") && !object.contains("value")) continue;
+ fields.push_back(
+ {object.value("label").toString(), object.value("value").toString()});
+ }
+ return fields;
+}
+
+auto ParsePacket(const QJsonObject& object) -> PGPInspectPacket {
+ PGPInspectPacket packet;
+ packet.tag = object.value("tag").toInt(-1);
+ packet.tag_name = object.value("tagName").toString();
+ packet.offset = static_cast<qint64>(object.value("offset").toDouble());
+ packet.header_version = object.value("headerVersion").toString();
+ packet.header_length =
+ static_cast<qint64>(object.value("headerLength").toDouble());
+ packet.body_length =
+ static_cast<qint64>(object.value("bodyLength").toDouble());
+ packet.length_type = object.value("lengthType").toString();
+ // Absent for the packet types that carry no version octet, which is a fact
+ // about the type rather than a defect in the document.
+ packet.version = object.value("version").toInt(-1);
+ packet.fields = ParseFields(object.value("fields"));
+ packet.error = object.value("error").toString();
+
+ for (const auto& child : object.value("children").toArray()) {
+ packet.children.push_back(ParsePacket(child.toObject()));
+ }
+ return packet;
+}
+
+auto ParseBlock(const QJsonObject& object) -> PGPInspectBlock {
+ PGPInspectBlock block;
+ block.kind = object.value("kind").toString();
+ block.offset = static_cast<qint64>(object.value("offset").toDouble());
+ block.error = object.value("error").toString();
+
+ if (object.value("armor").isObject()) {
+ const auto armor = object.value("armor").toObject();
+ block.has_armor = true;
+ block.armor.block_type = armor.value("blockType").toString();
+ block.armor.headers = ParseFields(armor.value("headers"));
+ block.armor.crc24 = armor.value("crc24").toString();
+ }
+
+ if (object.value("cleartext").isObject()) {
+ const auto cleartext = object.value("cleartext").toObject();
+ block.has_cleartext = true;
+ block.cleartext_text_size =
+ static_cast<qint64>(cleartext.value("textSize").toDouble());
+ block.cleartext_headers = ParseFields(cleartext.value("headers"));
+ }
+
+ for (const auto& packet : object.value("packets").toArray()) {
+ block.packets.push_back(ParsePacket(packet.toObject()));
+ }
+ return block;
+}
+
+auto CountPackets(const QVector<PGPInspectPacket>& packets) -> int {
+ int count = 0;
+ for (const auto& packet : packets) {
+ count += 1 + CountPackets(packet.children);
+ }
+ return count;
+}
+
+} // namespace
+
+auto PGPInspectDocument::PacketCount() const -> int {
+ int count = 0;
+ for (const auto& block : blocks) count += CountPackets(block.packets);
+ return count;
+}
+
+auto ParsePGPInspectDocument(const QByteArray& json) -> PGPInspectDocument {
+ PGPInspectDocument document;
+
+ QJsonParseError error{};
+ const auto parsed = QJsonDocument::fromJson(json, &error);
+ if (error.error != QJsonParseError::NoError) {
+ document.parse_error = error.errorString();
+ return document;
+ }
+ if (!parsed.isObject()) {
+ document.parse_error =
+ QCoreApplication::translate("GTrC", "The inspector returned no data.");
+ return document;
+ }
+
+ const auto object = parsed.object();
+ document.valid = true;
+ document.format = object.value("format").toString();
+ document.size = static_cast<qint64>(object.value("size").toDouble());
+
+ for (const auto& note : object.value("errors").toArray()) {
+ document.errors.append(note.toString());
+ }
+ for (const auto& block : object.value("blocks").toArray()) {
+ document.blocks.push_back(ParseBlock(block.toObject()));
+ }
+ return document;
+}
+
+auto PGPInspectHasStructure(const PGPInspectDocument& document) -> bool {
+ return document.valid && document.PacketCount() > 0;
+}
+
+auto PGPInspectPacketSummary(const PGPInspectPacket& packet, int index)
+ -> QString {
+ // "#1 Signature (tag 2) v4 @0x0000 189 B". The offset is hexadecimal
+ // because that is what every other packet dumper prints, and what a reader
+ // comparing two tools needs in order to line them up.
+ auto summary = QCoreApplication::translate("GTrC", "#%1 %2 (tag %3)")
+ .arg(index)
+ .arg(packet.tag_name.isEmpty()
+ ? QCoreApplication::translate("GTrC", "Unknown")
+ : packet.tag_name)
+ .arg(packet.tag);
+
+ if (packet.version >= 0) summary += QString(" v%1").arg(packet.version);
+
+ summary += QString(" @0x%1").arg(packet.offset, 4, 16, QChar('0'));
+ summary += QCoreApplication::translate("GTrC", " %1 B")
+ .arg(packet.header_length + packet.body_length);
+
+ if (packet.length_type != QLatin1String("fixed") &&
+ !packet.length_type.isEmpty()) {
+ summary += QString(" [%1]").arg(packet.length_type);
+ }
+ return summary;
+}
+
+auto PGPInspectBlockSummary(const PGPInspectBlock& block, int index)
+ -> QString {
+ if (block.kind == QLatin1String("cleartext")) {
+ return QCoreApplication::translate("GTrC",
+ "Block %1: cleartext-signed message")
+ .arg(index);
+ }
+ if (block.has_armor) {
+ return QCoreApplication::translate("GTrC", "Block %1: armored, %2")
+ .arg(index)
+ .arg(block.armor.block_type);
+ }
+ return QCoreApplication::translate("GTrC", "Block %1: binary packet stream")
+ .arg(index);
+}
diff --git a/src/m_pgp_inspect/PGPInspectModel.h b/src/m_pgp_inspect/PGPInspectModel.h
new file mode 100644
index 0000000..0a081aa
--- /dev/null
+++ b/src/m_pgp_inspect/PGPInspectModel.h
@@ -0,0 +1,141 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include <QByteArray>
+#include <QString>
+#include <QStringList>
+#include <QVector>
+
+/**
+ * @file PGPInspectModel.h
+ * @brief The inspector's document, and the JSON that carries it.
+ *
+ * Deliberately free of every SDK and widget symbol. The parsing is the part
+ * with rules worth checking -- absent fields, malformed documents, the name
+ * tables -- and keeping it here is what lets a plain gtest binary check them
+ * without a GUI thread or a loaded host. The dialog stays a dumb view over
+ * what comes out.
+ */
+
+/// One label / value row in a packet's detail table.
+struct PGPInspectField {
+ QString label;
+ QString value;
+};
+
+/// A packet, and whatever is nested inside it.
+struct PGPInspectPacket {
+ /// The numeric packet type ID; -1 when the document did not say.
+ int tag = -1;
+ QString tag_name;
+ qint64 offset = 0;
+ /// "old" or "new".
+ QString header_version;
+ qint64 header_length = 0;
+ qint64 body_length = 0;
+ /// "fixed", "partial" or "indeterminate".
+ QString length_type;
+ /// The body's own version octet; -1 for the types that carry none.
+ int version = -1;
+ QVector<PGPInspectField> fields;
+ QVector<PGPInspectPacket> children;
+ /// Why this packet could not be decoded, empty when it could.
+ QString error;
+
+ [[nodiscard]] auto Malformed() const -> bool { return !error.isEmpty(); }
+};
+
+/// The armor envelope of a block.
+struct PGPInspectArmor {
+ QString block_type;
+ QVector<PGPInspectField> headers;
+ /// "ok", "mismatch" or "absent".
+ QString crc24;
+};
+
+/// One armor block, or the whole input when it carries none.
+struct PGPInspectBlock {
+ /// "binary", "armored" or "cleartext".
+ QString kind;
+ qint64 offset = 0;
+ bool has_armor = false;
+ PGPInspectArmor armor;
+ bool has_cleartext = false;
+ qint64 cleartext_text_size = 0;
+ QVector<PGPInspectField> cleartext_headers;
+ QVector<PGPInspectPacket> packets;
+ QString error;
+};
+
+/// What an inspected input turned out to be.
+struct PGPInspectDocument {
+ /// False when the JSON could not be read at all; see @ref parse_error.
+ bool valid = false;
+ QString format;
+ qint64 size = 0;
+ QVector<PGPInspectBlock> blocks;
+ QStringList errors;
+ QString parse_error;
+
+ /// Every packet in the document, containers before their contents.
+ [[nodiscard]] auto PacketCount() const -> int;
+};
+
+/**
+ * @brief Read the inspector's JSON document.
+ *
+ * Never throws and never returns a half-built document: anything it cannot
+ * read leaves @ref PGPInspectDocument::valid false with a reason, and a field
+ * that is merely missing takes its default rather than invalidating the rest.
+ */
+auto ParsePGPInspectDocument(const QByteArray& json) -> PGPInspectDocument;
+
+/**
+ * @brief The one-line summary a packet gets in the tree.
+ *
+ * @param packet the packet to describe
+ * @param index its 1-based position among its siblings
+ */
+auto PGPInspectPacketSummary(const PGPInspectPacket& packet, int index)
+ -> QString;
+
+/// A block's one-line summary: what kind it is, and what it wraps.
+auto PGPInspectBlockSummary(const PGPInspectBlock& block, int index) -> QString;
+
+/**
+ * @brief Whether a document holds a structure worth opening a window for.
+ *
+ * A valid document with no packets in it is the ordinary answer for plain
+ * text, a half-written note, or anything else that simply is not OpenPGP --
+ * so it is what the menu entry is greyed out on. The distinction matters:
+ * "this is not OpenPGP" is a finding, and offering to display it would make
+ * the user open a window to be told nothing.
+ */
+auto PGPInspectHasStructure(const PGPInspectDocument& document) -> bool;
diff --git a/src/m_pgp_inspect/PGPInspectModule.cpp b/src/m_pgp_inspect/PGPInspectModule.cpp
new file mode 100644
index 0000000..22dcccd
--- /dev/null
+++ b/src/m_pgp_inspect/PGPInspectModule.cpp
@@ -0,0 +1,132 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include <GFModule.h>
+
+#include <QAction>
+#include <QApplication>
+#include <QMainWindow>
+#include <QMenu>
+#include <QPointer>
+
+#include "GFModuleIdentity.h"
+#include "PGPInspectDialog.h"
+
+namespace {
+
+/// The ceiling on what is read out of a tab. Past this, the menu entry stays
+/// greyed out rather than making every menu opening pay for a packet walk.
+constexpr qint64 kMaxInspectSize = 16LL * 1024 * 1024;
+
+/// The current tab's document, or an invalid one when there is nothing to
+/// show. Cheap enough to run when the menu opens: the walk is a linear pass
+/// over at most kMaxInspectSize bytes and decrypts nothing.
+auto InspectCurrentTab() -> PGPInspectDocument {
+ const auto bytes = PGPInspectCurrentTabBytes();
+ if (bytes.isEmpty() || bytes.size() > kMaxInspectSize) return {};
+ return PGPInspectBytes(bytes);
+}
+
+void RaiseInspectDialog(QWidget *parent) {
+ const auto bytes = PGPInspectCurrentTabBytes();
+ auto *dialog =
+ new PGPInspectDialog(PGPInspectBytes(bytes), bytes.size(), parent);
+ dialog->setModal(false);
+ dialog->show();
+}
+
+} // namespace
+
+auto OnActivate() -> GFResult {
+ // Nothing to register with the host: the module owns no settings page, no
+ // tab view and no metatype. Its whole surface is one menu action, added
+ // when the menu is mounted.
+ return GFResult::Ok();
+}
+
+auto OnMainWindowMenuMounted(const GFEvent &event) -> GFEventResult {
+ QMainWindow *main_window = nullptr;
+ if (auto r = event.RequireGui("main_window", main_window); !r.ok) return r;
+
+ QMenu *advance_menu = nullptr;
+ if (auto r = event.RequireGui("advance_menu", advance_menu); !r.ok) return r;
+
+ LOG_DEBUG("adding openpgp structure inspector to the advanced menu");
+
+ QMetaObject::invokeMethod(
+ QApplication::instance(),
+ [=]() -> void {
+ auto *action = new QAction(
+ QCoreApplication::translate("GTrC", "Inspect OpenPGP Structure"),
+ nullptr);
+ action->setToolTip(QCoreApplication::translate(
+ "GTrC",
+ "Show the packet structure of the current tab, without "
+ "decrypting it"));
+ QObject::connect(action, &QAction::triggered, main_window,
+ [=]() { RaiseInspectDialog(main_window); });
+ advance_menu->addAction(action);
+
+ // Decided when the menu opens rather than once at mount time: what
+ // the current tab holds changes constantly, and an entry that is
+ // enabled over plain text promises a window that would have nothing
+ // in it.
+ QPointer<QAction> guarded(action);
+ QObject::connect(advance_menu, &QMenu::aboutToShow, action, [guarded] {
+ if (guarded.isNull()) return;
+ guarded->setEnabled(PGPInspectHasStructure(InspectCurrentTab()));
+ });
+ },
+ Qt::BlockingQueuedConnection);
+
+ return GFEventResult::Ok();
+}
+
+// The module's whole framework surface: which events it handles, what runs at
+// each lifecycle point, and one forwarder to the runtime that implements all
+// of it. Everything above is business logic.
+constexpr GFEventBinding kEvents[] = {
+ {"MAINWINDOW_MENU_MOUNTED", &OnMainWindowMenuMounted},
+};
+
+constexpr GFModuleHooks kHooks = {
+ sizeof(GFModuleHooks),
+ GF_MODULE_ID,
+ GF_MODULE_VERSION,
+ GF_MODULE_TRANSLATION_CONTEXT,
+ &OnActivate,
+ nullptr, // nothing registered with the host, so nothing to undo
+ nullptr,
+ kEvents,
+ std::size(kEvents),
+};
+
+extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi)
+ -> const GFModuleApi * {
+ return GFModuleRuntimeGetApi(abi, &kHooks);
+}
diff --git a/src/m_pgp_inspect/module.json b/src/m_pgp_inspect/module.json
new file mode 100644
index 0000000..92094bf
--- /dev/null
+++ b/src/m_pgp_inspect/module.json
@@ -0,0 +1,15 @@
+{
+ "schema_version": 1,
+ "id": "com.bktus.gpgfrontend.module.pgp_inspect",
+ "version": "1.0.0",
+ "name": "PGPInspect",
+ "description": "Inspect the packet structure of any OpenPGP data.",
+ "author": "Saturneric",
+ "capabilities": [
+ "ui"
+ ],
+ "events": [
+ "MAINWINDOW_MENU_MOUNTED"
+ ],
+ "translation_context": "ModulePGPInspect"
+}
diff --git a/src/m_pgp_inspect/test/CMakeLists.txt b/src/m_pgp_inspect/test/CMakeLists.txt
new file mode 100644
index 0000000..a213bc7
--- /dev/null
+++ b/src/m_pgp_inspect/test/CMakeLists.txt
@@ -0,0 +1,53 @@
+# Copyright (C) 2021-2024 Saturneric <[email protected]>
+#
+# This file is part of GpgFrontend.
+#
+# GpgFrontend 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.
+#
+# GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+#
+# The initial version of the source code is inherited from
+# the gpg4usb project, which is under GPL-3.0-or-later.
+#
+# All the source code of GpgFrontend was modified and released by
+# Saturneric <[email protected]> starting on May 12, 2021.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Unit tests for the SDK-free half of the OpenPGP structure inspector.
+#
+# PGPInspectModel.cpp carries no GF SDK symbol and no widget, so it compiles
+# straight into a plain gtest binary with nothing but Qt Core behind it. That
+# is the whole reason the split exists; keep it that way, or this target stops
+# linking.
+
+if(NOT TARGET GTest::gtest)
+ find_package(GTest QUIET)
+endif()
+
+if(NOT TARGET GTest::gtest)
+ message(STATUS "GTest not found; skipping OpenPGP inspector module tests")
+ return()
+endif()
+
+add_executable(gf_mod_pgp_inspect_test
+ PGPInspectModelTest.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/../PGPInspectModel.cpp)
+
+target_include_directories(gf_mod_pgp_inspect_test PRIVATE
+ "${CMAKE_CURRENT_SOURCE_DIR}/..")
+
+target_link_libraries(gf_mod_pgp_inspect_test PRIVATE
+ GTest::gtest GTest::gtest_main Qt::Core)
+
+set_target_properties(gf_mod_pgp_inspect_test PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
diff --git a/src/m_pgp_inspect/test/PGPInspectModelTest.cpp b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp
new file mode 100644
index 0000000..3815176
--- /dev/null
+++ b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp
@@ -0,0 +1,298 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * This file is part of GpgFrontend.
+ *
+ * GpgFrontend 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.
+ *
+ * GpgFrontend 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 GpgFrontend. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * The initial version of the source code is inherited from
+ * the gpg4usb project, which is under GPL-3.0-or-later.
+ *
+ * All the source code of GpgFrontend was modified and released by
+ * Saturneric <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include <gtest/gtest.h>
+
+#include <QCoreApplication>
+
+#include "PGPInspectModel.h"
+
+namespace {
+
+/// A minimal but complete document, in the shape the Rust walk emits.
+constexpr const char* kDocument = R"({
+ "format": "armored",
+ "size": 325,
+ "errors": [],
+ "blocks": [
+ {
+ "kind": "armored",
+ "offset": 0,
+ "armor": {
+ "blockType": "PGP SIGNATURE",
+ "headers": [{"label": "Version", "value": "GnuPG v2"}],
+ "crc24": "ok"
+ },
+ "packets": [
+ {
+ "tag": 8,
+ "tagName": "Compressed Data",
+ "offset": 0,
+ "headerVersion": "new",
+ "headerLength": 2,
+ "bodyLength": 189,
+ "lengthType": "fixed",
+ "fields": [{"label": "Compression Algorithm", "value": "ZIP"}],
+ "children": [
+ {
+ "tag": 2,
+ "tagName": "Signature",
+ "offset": 0,
+ "headerVersion": "old",
+ "headerLength": 3,
+ "bodyLength": 100,
+ "lengthType": "fixed",
+ "version": 4,
+ "fields": [{"label": "Hash Algorithm", "value": "Sha256"}],
+ "children": [],
+ "error": null
+ }
+ ],
+ "error": null
+ }
+ ],
+ "error": null
+ }
+ ]
+})";
+
+} // namespace
+
+// -- reading the document ----------------------------------------------------
+
+TEST(PGPInspectModelTest, ReadsTheDocumentShape) {
+ const auto document = ParsePGPInspectDocument(kDocument);
+
+ ASSERT_TRUE(document.valid);
+ EXPECT_EQ(document.format, QString("armored"));
+ EXPECT_EQ(document.size, 325);
+ ASSERT_EQ(document.blocks.size(), 1);
+ EXPECT_TRUE(document.errors.isEmpty());
+}
+
+TEST(PGPInspectModelTest, ReadsTheArmorEnvelope) {
+ const auto document = ParsePGPInspectDocument(kDocument);
+ const auto& block = document.blocks.at(0);
+
+ ASSERT_TRUE(block.has_armor);
+ EXPECT_EQ(block.armor.block_type, QString("PGP SIGNATURE"));
+ EXPECT_EQ(block.armor.crc24, QString("ok"));
+ ASSERT_EQ(block.armor.headers.size(), 1);
+ EXPECT_EQ(block.armor.headers.at(0).label, QString("Version"));
+}
+
+TEST(PGPInspectModelTest, NestsChildrenUnderTheirContainer) {
+ // The recursion is the reason the tree exists; a flat read would lose it.
+ const auto document = ParsePGPInspectDocument(kDocument);
+ const auto& packets = document.blocks.at(0).packets;
+
+ ASSERT_EQ(packets.size(), 1);
+ EXPECT_EQ(packets.at(0).tag, 8);
+ ASSERT_EQ(packets.at(0).children.size(), 1);
+ EXPECT_EQ(packets.at(0).children.at(0).tag, 2);
+ EXPECT_EQ(packets.at(0).children.at(0).version, 4);
+}
+
+TEST(PGPInspectModelTest, CountsNestedPacketsToo) {
+ EXPECT_EQ(ParsePGPInspectDocument(kDocument).PacketCount(), 2);
+}
+
+TEST(PGPInspectModelTest, KeepsTheFramingFiguresVerbatim) {
+ const auto& packet =
+ ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0);
+
+ EXPECT_EQ(packet.offset, 0);
+ EXPECT_EQ(packet.header_length, 2);
+ EXPECT_EQ(packet.body_length, 189);
+ EXPECT_EQ(packet.header_version, QString("new"));
+ EXPECT_EQ(packet.length_type, QString("fixed"));
+}
+
+TEST(PGPInspectModelTest, AbsentVersionReadsAsMinusOne) {
+ // Most container packets carry no version octet. That is a fact about the
+ // type, so it must not look like a defect in the document.
+ const auto& packet =
+ ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0);
+ EXPECT_EQ(packet.version, -1);
+ EXPECT_FALSE(packet.Malformed());
+}
+
+// -- the shapes that are not the happy one ------------------------------------
+
+TEST(PGPInspectModelTest, MalformedJsonIsReportedNotThrown) {
+ const auto document = ParsePGPInspectDocument("{ this is not json");
+
+ EXPECT_FALSE(document.valid);
+ EXPECT_FALSE(document.parse_error.isEmpty());
+ EXPECT_TRUE(document.blocks.isEmpty());
+}
+
+TEST(PGPInspectModelTest, EmptyInputIsReportedNotThrown) {
+ const auto document = ParsePGPInspectDocument({});
+
+ EXPECT_FALSE(document.valid);
+ EXPECT_FALSE(document.parse_error.isEmpty());
+}
+
+TEST(PGPInspectModelTest, ANonObjectDocumentIsRefused) {
+ EXPECT_FALSE(ParsePGPInspectDocument("[1, 2, 3]").valid);
+}
+
+TEST(PGPInspectModelTest, AnEmptyDocumentIsValidAndEmpty) {
+ // "nothing recognisable here" is a finding the inspector reports, not a
+ // failure to read its answer.
+ const auto document = ParsePGPInspectDocument(
+ R"({"format":"binary","size":4,"blocks":[],"errors":["not OpenPGP data"]})");
+
+ ASSERT_TRUE(document.valid);
+ EXPECT_TRUE(document.blocks.isEmpty());
+ ASSERT_EQ(document.errors.size(), 1);
+ EXPECT_EQ(document.errors.at(0), QString("not OpenPGP data"));
+}
+
+TEST(PGPInspectModelTest, APacketErrorSurvivesIntoTheTree) {
+ const auto document = ParsePGPInspectDocument(
+ R"({"blocks":[{"packets":[{"tag":2,"error":"the packet body is truncated"}]}]})");
+
+ ASSERT_EQ(document.blocks.size(), 1);
+ const auto& packet = document.blocks.at(0).packets.at(0);
+ EXPECT_TRUE(packet.Malformed());
+ EXPECT_EQ(packet.error, QString("the packet body is truncated"));
+}
+
+TEST(PGPInspectModelTest, MissingFieldsTakeTheirDefaults) {
+ // A document from an older or newer engine must degrade field by field
+ // rather than costing the reader the whole tree.
+ const auto document =
+ ParsePGPInspectDocument(R"({"blocks":[{"packets":[{}]}]})");
+
+ ASSERT_TRUE(document.valid);
+ const auto& packet = document.blocks.at(0).packets.at(0);
+ EXPECT_EQ(packet.tag, -1);
+ EXPECT_TRUE(packet.tag_name.isEmpty());
+ EXPECT_EQ(packet.body_length, 0);
+}
+
+TEST(PGPInspectModelTest, ADetailRowWithNeitherHalfIsDropped) {
+ const auto document = ParsePGPInspectDocument(
+ R"({"blocks":[{"packets":[{"fields":[{},{"label":"A","value":"B"}]}]}]})");
+
+ const auto& fields = document.blocks.at(0).packets.at(0).fields;
+ ASSERT_EQ(fields.size(), 1);
+ EXPECT_EQ(fields.at(0).label, QString("A"));
+}
+
+TEST(PGPInspectModelTest, ReadsTheCleartextHalf) {
+ const auto document = ParsePGPInspectDocument(
+ R"({"format":"cleartext","blocks":[{"kind":"cleartext","cleartext":{"textSize":42,"headers":[{"label":"Hash","value":"SHA256"}]},"packets":[]}]})");
+
+ const auto& block = document.blocks.at(0);
+ ASSERT_TRUE(block.has_cleartext);
+ EXPECT_EQ(block.cleartext_text_size, 42);
+ ASSERT_EQ(block.cleartext_headers.size(), 1);
+ EXPECT_EQ(block.cleartext_headers.at(0).value, QString("SHA256"));
+}
+
+// -- whether there is anything to show ----------------------------------------
+
+TEST(PGPInspectModelTest, ADocumentWithPacketsHasStructure) {
+ EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument(kDocument)));
+}
+
+TEST(PGPInspectModelTest, PlainTextHasNoStructure) {
+ // The menu entry is greyed out on exactly this: a valid document saying the
+ // input is not OpenPGP. Opening a window to be told nothing is worse than
+ // an entry that cannot be clicked.
+ EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument(
+ R"({"format":"binary","size":9,"blocks":[],"errors":["not OpenPGP data"]})")));
+}
+
+TEST(PGPInspectModelTest, AnUnreadableDocumentHasNoStructure) {
+ EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument("not json")));
+ EXPECT_FALSE(PGPInspectHasStructure({}));
+}
+
+TEST(PGPInspectModelTest, ABlockWithNoPacketsHasNoStructure) {
+ // An armor envelope whose body decoded to nothing: the header parsed, so
+ // the document is valid, but there is still no packet tree to render.
+ EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument(
+ R"({"format":"armored","blocks":[{"kind":"armored","packets":[]}]})")));
+}
+
+TEST(PGPInspectModelTest, ANestedPacketAloneIsStructureEnough) {
+ EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument(
+ R"({"blocks":[{"packets":[{"tag":8,"children":[{"tag":2}]}]}]})")));
+}
+
+// -- the summaries the tree shows ---------------------------------------------
+
+TEST(PGPInspectModelTest, APacketSummaryNamesTagVersionOffsetAndSize) {
+ const auto& packet =
+ ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0).children.at(
+ 0);
+ const auto summary = PGPInspectPacketSummary(packet, 1);
+
+ EXPECT_TRUE(summary.contains("#1"));
+ EXPECT_TRUE(summary.contains("Signature"));
+ EXPECT_TRUE(summary.contains("tag 2"));
+ EXPECT_TRUE(summary.contains("v4"));
+ EXPECT_TRUE(summary.contains("@0x0000"));
+ // Header plus body: the bytes this packet occupies on the wire.
+ EXPECT_TRUE(summary.contains("103"));
+}
+
+TEST(PGPInspectModelTest, ASummaryFlagsANonFixedLength) {
+ PGPInspectPacket packet;
+ packet.tag = 11;
+ packet.tag_name = "Literal Data";
+ packet.length_type = "partial";
+
+ EXPECT_TRUE(PGPInspectPacketSummary(packet, 2).contains("partial"));
+}
+
+TEST(PGPInspectModelTest, ASummaryOfANamelessPacketStillReads) {
+ PGPInspectPacket packet;
+ packet.tag = 60;
+
+ const auto summary = PGPInspectPacketSummary(packet, 1);
+ EXPECT_TRUE(summary.contains("Unknown"));
+ EXPECT_TRUE(summary.contains("tag 60"));
+}
+
+TEST(PGPInspectModelTest, ABlockSummaryDistinguishesTheThreeKinds) {
+ const auto armored = ParsePGPInspectDocument(kDocument).blocks.at(0);
+ EXPECT_TRUE(PGPInspectBlockSummary(armored, 1).contains("PGP SIGNATURE"));
+
+ PGPInspectBlock cleartext;
+ cleartext.kind = "cleartext";
+ EXPECT_TRUE(PGPInspectBlockSummary(cleartext, 1).contains("cleartext"));
+
+ PGPInspectBlock binary;
+ binary.kind = "binary";
+ EXPECT_TRUE(PGPInspectBlockSummary(binary, 1).contains("binary"));
+}
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.de_DE.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.en_US.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.es_ES.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.fr_FR.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.it_IT.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.pl_PL.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.ru_RU.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_CN.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts
new file mode 100644
index 0000000..7b67b95
--- /dev/null
+++ b/src/m_pgp_inspect/ts/ModulePGPInspect.zh_TW.ts
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+<context>
+ <name>GTrC</name>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="82"/>
+ <source>OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="99"/>
+ <source>Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="100"/>
+ <source>Value</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="93"/>
+ <source>Search packets, fields and values</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="122"/>
+ <source>The current tab holds no OpenPGP data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="137"/>
+ <source>Armor Type</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="140"/>
+ <source>CRC24</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="144"/>
+ <source>Signed Text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="../PGPInspectDialog.cpp" line="168"/>
+ <source>%1, %2, %n packet(s)</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectDialog.cpp" line="150"/>
+ <location filename="../PGPInspectDialog.cpp" line="192"/>
+ <source>Problem</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="130"/>
+ <source>The inspector returned no data.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="157"/>
+ <source>#%1 %2 (tag %3)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="160"/>
+ <source>Unknown</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="167"/>
+ <source> %1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="180"/>
+ <source>Block %1: cleartext-signed message</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="185"/>
+ <source>Block %1: armored, %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModel.cpp" line="189"/>
+ <source>Block %1: binary packet stream</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="85"/>
+ <source>Inspect OpenPGP Structure</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="../PGPInspectModule.cpp" line="87"/>
+ <source>Show the packet structure of the current tab, without decrypting it</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>