app/core/ProfileActivation.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ProfileActivation.h" | ||
| 2 | #include "app/MailtoUrl.h" | ||
| 3 | #include <QCryptographicHash> | ||
| 4 | #include <QDeadlineTimer> | ||
| 5 | #include <algorithm> | ||
| 6 | #include <QDir> | ||
| 7 | #include <QFileInfo> | ||
| 8 | #include <QJsonDocument> | ||
| 9 | #include <QJsonObject> | ||
| 10 | #include <QLocalSocket> | ||
| 11 | #include <QStandardPaths> | ||
| 12 | #include <QThread> | ||
| 13 | #include <QTimer> | ||
| 14 | |||
| 15 | namespace { | ||
| 16 | constexpr qsizetype MaximumPacket = 256 * 1024; | ||
| 17 | constexpr qsizetype MaximumUrl = 65536; | ||
| 18 | } | ||
| 19 | |||
| 20 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | ProfileActivation::ProfileActivation(QObject *parent) : QObject(parent) { |
| 21 |
1/1✓ Branch 7 → 8 taken 5 times.
|
5 | m_server.setSocketOptions(QLocalServer::UserAccessOption); |
| 22 |
1/1✓ Branch 8 → 9 taken 5 times.
|
5 | m_server.setMaxPendingConnections(8); |
| 23 |
1/1✓ Branch 9 → 10 taken 5 times.
|
5 | connect(&m_server, &QLocalServer::newConnection, this, [this] { |
| 24 |
2/2✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 16 taken 11 times.
|
22 | while (auto *client = m_server.nextPendingConnection()) { |
| 25 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 11 times.
|
11 | if (m_clients >= 8) { client->abort(); client->deleteLater(); continue; } |
| 26 | 11 | ++m_clients; | |
| 27 | 11 | client->setReadBufferSize(MaximumPacket + 1); | |
| 28 |
1/1✓ Branch 9 → 10 taken 11 times.
|
11 | connect(client, &QLocalSocket::disconnected, this, [this, client] { |
| 29 | 11 | --m_clients; | |
| 30 | 11 | client->deleteLater(); | |
| 31 | 11 | }); | |
| 32 |
1/1✓ Branch 11 → 12 taken 11 times.
|
11 | QTimer::singleShot(5000, client, [client] { client->abort(); }); |
| 33 |
1/1✓ Branch 12 → 13 taken 11 times.
|
11 | connect(client, &QLocalSocket::readyRead, this, [this, client] { |
| 34 |
3/4✓ Branch 2 → 3 taken 12 times.
✓ Branch 3 → 4 taken 12 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12 times.
|
13 | if (client->property("answered").toBool()) return; |
| 35 |
8/8✓ Branch 7 → 8 taken 12 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 12 taken 10 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 11 times.
|
12 | if (!client->canReadLine() && client->bytesAvailable() <= MaximumPacket) return; |
| 36 |
1/1✓ Branch 16 → 17 taken 11 times.
|
11 | client->setProperty("answered", true); |
| 37 |
1/1✓ Branch 18 → 19 taken 11 times.
|
11 | const auto bytes = client->readAll(); |
| 38 |
1/1✓ Branch 19 → 20 taken 11 times.
|
11 | const auto document = QJsonDocument::fromJson(bytes); |
| 39 |
1/1✓ Branch 20 → 21 taken 11 times.
|
11 | const auto request = document.object(); |
| 40 |
3/3✓ Branch 21 → 22 taken 11 times.
✓ Branch 22 → 23 taken 11 times.
✓ Branch 23 → 24 taken 11 times.
|
11 | const auto url = request.value("mailto").toString(); |
| 41 | // Protocol v2 carries the forwarder's xdg-activation token; v1 | ||
| 42 | // requests (three keys, no token) stay admissible so an old | ||
| 43 | // binary can still activate a new owner. | ||
| 44 |
3/3✓ Branch 26 → 27 taken 11 times.
✓ Branch 27 → 28 taken 11 times.
✓ Branch 28 → 29 taken 11 times.
|
11 | const int version = request.value("version").toInt(); |
| 45 |
3/3✓ Branch 31 → 32 taken 11 times.
✓ Branch 32 → 33 taken 11 times.
✓ Branch 33 → 34 taken 11 times.
|
11 | const QString token = request.value("activationToken").toString(); |
| 46 |
4/4✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 44 taken 8 times.
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 45 taken 1 time.
|
16 | const bool tokenValid = token.isEmpty() || |
| 47 |
3/3✓ Branch 42 → 43 taken 2 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 1 time.
|
5 | (token.size() <= 4096 && std::none_of(token.cbegin(), token.cend(), |
| 48 | 13 | [](const QChar c) { | |
| 49 |
3/4✓ Branch 3 → 4 taken 13 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 12 times.
|
13 | return c.isNull() || c.category() == QChar::Other_Control; |
| 50 | 11 | })); | |
| 51 |
4/5✓ Branch 48 → 49 taken 10 times.
✓ Branch 49 → 50 taken 10 times.
✗ Branch 49 → 74 not taken.
✓ Branch 54 → 55 taken 8 times.
✓ Branch 54 → 74 taken 2 times.
|
20 | const bool valid = bytes.size() <= MaximumPacket && document.isObject() && |
| 52 |
5/5✓ Branch 50 → 51 taken 10 times.
✓ Branch 51 → 52 taken 6 times.
✓ Branch 51 → 53 taken 4 times.
✓ Branch 55 → 56 taken 5 times.
✓ Branch 55 → 57 taken 3 times.
|
18 | request.size() == (version == 2 ? 4 : 3) && |
| 53 |
1/2✓ Branch 56 → 57 taken 5 times.
✗ Branch 56 → 74 not taken.
|
5 | (version == 1 || version == 2) && |
| 54 |
7/11✓ Branch 57 → 58 taken 8 times.
✓ Branch 58 → 59 taken 8 times.
✓ Branch 59 → 60 taken 8 times.
✓ Branch 60 → 61 taken 7 times.
✓ Branch 60 → 74 taken 1 time.
✓ Branch 83 → 84 taken 8 times.
✓ Branch 83 → 85 taken 3 times.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 153 not taken.
✗ Branch 155 → 156 not taken.
✗ Branch 155 → 157 not taken.
|
19 | request.value("compose").isBool() && |
| 55 |
11/17✓ Branch 61 → 62 taken 7 times.
✓ Branch 62 → 63 taken 7 times.
✓ Branch 63 → 64 taken 7 times.
✓ Branch 64 → 65 taken 7 times.
✗ Branch 64 → 74 not taken.
✓ Branch 66 → 67 taken 7 times.
✗ Branch 66 → 74 not taken.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 68 → 72 taken 4 times.
✓ Branch 79 → 80 taken 7 times.
✓ Branch 79 → 81 taken 4 times.
✓ Branch 81 → 82 taken 8 times.
✓ Branch 81 → 83 taken 3 times.
✗ Branch 143 → 144 not taken.
✗ Branch 143 → 145 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
|
25 | request.value("mailto").isString() && url.size() <= MaximumUrl && |
| 56 |
11/13✓ Branch 47 → 48 taken 10 times.
✓ Branch 47 → 74 taken 1 time.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 74 taken 1 time.
✓ Branch 72 → 73 taken 4 times.
✓ Branch 72 → 74 taken 2 times.
✓ Branch 75 → 76 taken 3 times.
✓ Branch 75 → 77 taken 8 times.
✓ Branch 77 → 78 taken 7 times.
✓ Branch 77 → 79 taken 4 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
|
28 | (url.isEmpty() || MailtoRequest::parse(url)) && tokenValid; |
| 57 |
3/4✓ Branch 85 → 86 taken 4 times.
✓ Branch 85 → 94 taken 7 times.
✓ Branch 87 → 88 taken 4 times.
✗ Branch 87 → 94 not taken.
|
15 | const bool admitted = valid && m_handler && |
| 58 |
10/14✓ Branch 88 → 89 taken 4 times.
✓ Branch 89 → 90 taken 4 times.
✓ Branch 90 → 91 taken 4 times.
✓ Branch 91 → 92 taken 4 times.
✓ Branch 92 → 93 taken 3 times.
✓ Branch 92 → 94 taken 1 time.
✓ Branch 95 → 96 taken 4 times.
✓ Branch 95 → 97 taken 7 times.
✓ Branch 97 → 98 taken 4 times.
✓ Branch 97 → 99 taken 7 times.
✗ Branch 159 → 160 not taken.
✗ Branch 159 → 161 not taken.
✗ Branch 163 → 164 not taken.
✗ Branch 163 → 165 not taken.
|
15 | m_handler(url, request.value("compose").toBool(), token); |
| 59 |
5/5✓ Branch 99 → 100 taken 3 times.
✓ Branch 99 → 101 taken 8 times.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 101 → 103 taken 7 times.
✓ Branch 105 → 106 taken 11 times.
|
11 | client->write(admitted ? "ok\n" : valid ? "busy\n" : "invalid\n"); |
| 60 |
1/1✓ Branch 106 → 107 taken 11 times.
|
11 | client->disconnectFromServer(); |
| 61 | 11 | }); | |
| 62 | 11 | } | |
| 63 | 11 | }); | |
| 64 | 5 | } | |
| 65 | |||
| 66 | 22 | QString ProfileActivation::endpoint(const QString &profile) { | |
| 67 |
2/2✓ Branch 2 → 3 taken 22 times.
✓ Branch 3 → 4 taken 22 times.
|
22 | const auto canonical = QFileInfo(profile).canonicalFilePath(); |
| 68 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 22 times.
|
22 | if (canonical.isEmpty()) return {}; |
| 69 |
1/1✓ Branch 8 → 9 taken 22 times.
|
22 | const auto root = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); |
| 70 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 22 times.
|
22 | if (root.isEmpty()) return {}; |
| 71 |
4/4✓ Branch 12 → 13 taken 22 times.
✓ Branch 14 → 15 taken 22 times.
✓ Branch 15 → 16 taken 22 times.
✓ Branch 16 → 17 taken 22 times.
|
22 | const auto hash = QCryptographicHash::hash(canonical.toUtf8(), QCryptographicHash::Sha256).toHex().left(32); |
| 72 |
4/4✓ Branch 20 → 21 taken 22 times.
✓ Branch 21 → 22 taken 22 times.
✓ Branch 25 → 26 taken 22 times.
✓ Branch 26 → 27 taken 22 times.
|
44 | return QDir(root).filePath(QStringLiteral("mailjd-") + QString::fromLatin1(hash)); |
| 73 | 22 | } | |
| 74 | |||
| 75 | 5 | bool ProfileActivation::listen(const ProfileLease &lease, Handler handler) { | |
| 76 |
7/11✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 4 times.
✓ Branch 9 → 10 taken 5 times.
✗ Branch 9 → 11 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 4 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
|
5 | if (lease.directory().isEmpty() || m_server.isListening()) return false; |
| 77 |
1/1✓ Branch 14 → 15 taken 4 times.
|
4 | const auto name = endpoint(lease.directory()); |
| 78 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 25 taken 4 times.
|
4 | if (name.isEmpty()) { m_error = QStringLiteral("No private activation endpoint is available."); return false; } |
| 79 | // Only the current lock owner can remove a socket left by a crashed owner. | ||
| 80 |
1/1✓ Branch 25 → 26 taken 4 times.
|
4 | QLocalServer::removeServer(name); |
| 81 | 4 | m_handler = std::move(handler); | |
| 82 |
2/4✓ Branch 28 → 29 taken 4 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 34 taken 4 times.
✗ Branch 30 → 31 not taken.
|
4 | if (!m_server.listen(name)) { m_error = m_server.errorString(); return false; } |
| 83 | 4 | m_error.clear(); | |
| 84 | 4 | return true; | |
| 85 | 4 | } | |
| 86 | |||
| 87 | 6 | bool ProfileActivation::forward(const QString &profile, const QString &mailto, bool compose, | |
| 88 | QString *error, int timeoutMs, | ||
| 89 | const QString &activationToken) { | ||
| 90 |
1/2✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 4 not taken.
|
3 | const auto fail = [error](const QString &message) { if (error) *error = message; return false; }; |
| 91 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | const auto name = endpoint(profile); |
| 92 |
9/15✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 13 taken 2 times.
✓ Branch 9 → 10 taken 4 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 4 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 2 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 24 taken 6 times.
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
|
6 | if (name.isEmpty() || mailto.size() > MaximumUrl || (!mailto.isEmpty() && !MailtoRequest::parse(mailto))) |
| 93 | ✗ | return fail(QStringLiteral("Invalid profile or desktop request.")); | |
| 94 |
1/1✓ Branch 24 → 25 taken 6 times.
|
6 | QLocalSocket client; |
| 95 |
1/1✓ Branch 25 → 26 taken 6 times.
|
6 | client.setReadBufferSize(32); |
| 96 |
1/1✓ Branch 26 → 27 taken 6 times.
|
6 | QDeadlineTimer deadline(qBound(1, timeoutMs, 10000)); |
| 97 | // Another process may own the lease while still constructing its scene. | ||
| 98 | // Retry only connection establishment, never a submitted request. | ||
| 99 | do { | ||
| 100 |
1/1✓ Branch 30 → 31 taken 276 times.
|
276 | client.connectToServer(name); |
| 101 |
3/3✓ Branch 33 → 34 taken 276 times.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 36 taken 272 times.
|
276 | if (client.waitForConnected(qMin<qint64>(250, deadline.remainingTime()))) break; |
| 102 |
1/1✓ Branch 36 → 37 taken 272 times.
|
272 | client.abort(); |
| 103 |
2/3✓ Branch 38 → 39 taken 272 times.
✗ Branch 38 → 40 not taken.
✓ Branch 39 → 40 taken 272 times.
|
272 | if (!deadline.hasExpired()) QThread::msleep(20); |
| 104 |
2/2✓ Branch 41 → 29 taken 270 times.
✓ Branch 41 → 42 taken 2 times.
|
272 | } while (!deadline.hasExpired()); |
| 105 |
3/3✓ Branch 42 → 43 taken 6 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 51 taken 4 times.
|
6 | if (client.state() != QLocalSocket::ConnectedState) |
| 106 | 2 | return fail(QStringLiteral("The running profile owner has no available activation channel.")); | |
| 107 | 4 | const auto bytes = QJsonDocument(QJsonObject{{"version", 2}, | |
| 108 |
3/5✓ Branch 56 → 57 taken 4 times.
✓ Branch 63 → 64 taken 16 times.
✓ Branch 63 → 65 taken 4 times.
✗ Branch 136 → 137 not taken.
✗ Branch 136 → 138 not taken.
|
24 | {"compose", compose}, {"mailto", mailto}, {"activationToken", activationToken}}) |
| 109 |
2/2✓ Branch 57 → 58 taken 4 times.
✓ Branch 58 → 59 taken 4 times.
|
4 | .toJson(QJsonDocument::Compact) + '\n'; |
| 110 |
4/7✓ Branch 66 → 67 taken 4 times.
✗ Branch 66 → 70 not taken.
✓ Branch 67 → 68 taken 4 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 4 times.
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 80 taken 4 times.
|
4 | if (bytes.size() > MaximumPacket || client.write(bytes) != bytes.size()) |
| 111 | ✗ | return fail(QStringLiteral("The desktop request could not be submitted.")); | |
| 112 |
6/7✓ Branch 85 → 86 taken 8 times.
✓ Branch 86 → 87 taken 4 times.
✓ Branch 86 → 90 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
✗ Branch 88 → 90 not taken.
✓ Branch 91 → 81 taken 4 times.
✓ Branch 91 → 92 taken 4 times.
|
8 | while (!client.canReadLine() && !deadline.hasExpired()) { |
| 113 |
2/3✓ Branch 82 → 83 taken 4 times.
✗ Branch 83 → 84 not taken.
✓ Branch 83 → 85 taken 4 times.
|
4 | if (!client.waitForReadyRead(deadline.remainingTime())) break; |
| 114 | } | ||
| 115 |
1/1✓ Branch 92 → 93 taken 4 times.
|
4 | const auto reply = client.readLine(32); |
| 116 |
3/4✓ Branch 94 → 95 taken 3 times.
✓ Branch 94 → 98 taken 1 time.
✓ Branch 95 → 96 taken 3 times.
✗ Branch 95 → 97 not taken.
|
4 | if (reply == "ok\n") { if (error) error->clear(); return true; } |
| 117 |
1/2✓ Branch 99 → 100 taken 1 time.
✗ Branch 99 → 107 not taken.
|
2 | if (reply == "busy\n") return fail(QStringLiteral("The running composer is busy. Finish its current action first.")); |
| 118 | ✗ | return fail(QStringLiteral("The running instance did not confirm the desktop request. It was not retried.")); | |
| 119 | 10 | } | |
| 120 |