data/AccountConfig.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "AccountConfig.h" | ||
| 2 | #include "CredentialStore.h" | ||
| 3 | #include "util/SafePathComponent.h" | ||
| 4 | |||
| 5 | #include <QCoreApplication> | ||
| 6 | #include <QCryptographicHash> | ||
| 7 | #include <QDir> | ||
| 8 | #include <QFile> | ||
| 9 | #include <QFileInfo> | ||
| 10 | #include <QJsonDocument> | ||
| 11 | #include <QJsonObject> | ||
| 12 | #include <QEventLoop> | ||
| 13 | #include <QLoggingCategory> | ||
| 14 | #include <QPointer> | ||
| 15 | #include <QSaveFile> | ||
| 16 | #include <QSet> | ||
| 17 | #include <QStandardPaths> | ||
| 18 | #include <QTimer> | ||
| 19 | #include <QUuid> | ||
| 20 | #include <utility> | ||
| 21 | |||
| 22 |
4/7✓ Branch 2 → 3 taken 28 times.
✓ Branch 2 → 8 taken 1864 times.
✓ Branch 4 → 5 taken 28 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 28 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
1892 | Q_LOGGING_CATEGORY(lcAccountConfig, "mailjd.accountconfig") |
| 23 | |||
| 24 | struct AccountCredentialMigration { | ||
| 25 | QString path; | ||
| 26 | QByteArray digest; | ||
| 27 | bool imapStored = false; | ||
| 28 | bool smtpStored = false; | ||
| 29 | }; | ||
| 30 | |||
| 31 | namespace { | ||
| 32 | |||
| 33 | 18 | bool writeAccountBytes(const QString &path, const QByteArray &bytes) { | |
| 34 |
1/1✓ Branch 2 → 3 taken 18 times.
|
18 | QSaveFile file(path); |
| 35 |
1/1✓ Branch 4 → 5 taken 18 times.
|
18 | return file.open(QIODevice::WriteOnly) && |
| 36 |
3/5✓ Branch 7 → 8 taken 18 times.
✓ Branch 8 → 9 taken 18 times.
✗ Branch 8 → 15 not taken.
✓ Branch 11 → 12 taken 18 times.
✗ Branch 11 → 15 not taken.
|
36 | file.setPermissions(QFile::ReadOwner | QFile::WriteOwner) && |
| 37 |
4/6✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 15 not taken.
✓ Branch 9 → 10 taken 18 times.
✓ Branch 12 → 13 taken 18 times.
✓ Branch 13 → 14 taken 18 times.
✗ Branch 13 → 15 not taken.
|
72 | file.write(bytes) == bytes.size() && file.commit(); |
| 38 | 18 | } | |
| 39 | |||
| 40 | 194 | QString legacyCredentialName(const AccountConfig &config) { | |
| 41 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 194 times.
|
194 | return config.id.isEmpty() ? config.name : config.id; |
| 42 | } | ||
| 43 | |||
| 44 | 54 | QByteArray accountMetadata(const AccountConfig &config) { | |
| 45 |
1/1✓ Branch 2 → 3 taken 54 times.
|
54 | QJsonObject imapObj; |
| 46 |
4/4✓ Branch 3 → 4 taken 54 times.
✓ Branch 4 → 5 taken 54 times.
✓ Branch 5 → 6 taken 54 times.
✓ Branch 6 → 7 taken 54 times.
|
54 | imapObj["host"] = config.imap.host; |
| 47 |
4/4✓ Branch 9 → 10 taken 54 times.
✓ Branch 10 → 11 taken 54 times.
✓ Branch 11 → 12 taken 54 times.
✓ Branch 12 → 13 taken 54 times.
|
54 | imapObj["port"] = config.imap.port; |
| 48 |
4/4✓ Branch 15 → 16 taken 54 times.
✓ Branch 16 → 17 taken 54 times.
✓ Branch 17 → 18 taken 54 times.
✓ Branch 18 → 19 taken 54 times.
|
54 | imapObj["security"] = config.imap.security; |
| 49 |
4/4✓ Branch 21 → 22 taken 54 times.
✓ Branch 22 → 23 taken 54 times.
✓ Branch 23 → 24 taken 54 times.
✓ Branch 24 → 25 taken 54 times.
|
54 | imapObj["username"] = config.imap.username; |
| 50 |
4/4✓ Branch 28 → 29 taken 54 times.
✓ Branch 29 → 30 taken 54 times.
✓ Branch 30 → 31 taken 54 times.
✓ Branch 31 → 32 taken 54 times.
|
54 | imapObj["password"] = QString(); |
| 51 |
2/2✓ Branch 36 → 37 taken 5 times.
✓ Branch 36 → 44 taken 49 times.
|
54 | if (!config.imap.credentialId.isEmpty()) |
| 52 |
4/4✓ Branch 37 → 38 taken 5 times.
✓ Branch 38 → 39 taken 5 times.
✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
|
5 | imapObj["credentialId"] = config.imap.credentialId; |
| 53 | |||
| 54 |
1/1✓ Branch 44 → 45 taken 54 times.
|
54 | QJsonObject smtpObj; |
| 55 |
4/4✓ Branch 45 → 46 taken 54 times.
✓ Branch 46 → 47 taken 54 times.
✓ Branch 47 → 48 taken 54 times.
✓ Branch 48 → 49 taken 54 times.
|
54 | smtpObj["host"] = config.smtp.host; |
| 56 |
4/4✓ Branch 51 → 52 taken 54 times.
✓ Branch 52 → 53 taken 54 times.
✓ Branch 53 → 54 taken 54 times.
✓ Branch 54 → 55 taken 54 times.
|
54 | smtpObj["port"] = config.smtp.port; |
| 57 |
4/4✓ Branch 57 → 58 taken 54 times.
✓ Branch 58 → 59 taken 54 times.
✓ Branch 59 → 60 taken 54 times.
✓ Branch 60 → 61 taken 54 times.
|
54 | smtpObj["security"] = config.smtp.security; |
| 58 |
4/4✓ Branch 63 → 64 taken 54 times.
✓ Branch 64 → 65 taken 54 times.
✓ Branch 65 → 66 taken 54 times.
✓ Branch 66 → 67 taken 54 times.
|
54 | smtpObj["username"] = config.smtp.username; |
| 59 |
4/4✓ Branch 70 → 71 taken 54 times.
✓ Branch 71 → 72 taken 54 times.
✓ Branch 72 → 73 taken 54 times.
✓ Branch 73 → 74 taken 54 times.
|
54 | smtpObj["password"] = QString(); |
| 60 |
2/2✓ Branch 78 → 79 taken 5 times.
✓ Branch 78 → 86 taken 49 times.
|
54 | if (!config.smtp.credentialId.isEmpty()) |
| 61 |
4/4✓ Branch 79 → 80 taken 5 times.
✓ Branch 80 → 81 taken 5 times.
✓ Branch 81 → 82 taken 5 times.
✓ Branch 82 → 83 taken 5 times.
|
5 | smtpObj["credentialId"] = config.smtp.credentialId; |
| 62 |
1/6✗ Branch 87 → 88 not taken.
✓ Branch 87 → 95 taken 54 times.
✗ Branch 88 → 89 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 90 → 91 not taken.
✗ Branch 91 → 92 not taken.
|
54 | if (config.smtp.sharesIncomingLogin()) smtpObj["login"] = config.smtp.login; |
| 63 | |||
| 64 |
1/1✓ Branch 95 → 96 taken 54 times.
|
54 | QJsonObject root; |
| 65 |
2/2✓ Branch 97 → 98 taken 4 times.
✓ Branch 97 → 158 taken 50 times.
|
54 | if (!config.sieve.isDefault()) { |
| 66 |
1/1✓ Branch 98 → 99 taken 4 times.
|
4 | QJsonObject sieveObj; |
| 67 |
4/4✓ Branch 99 → 100 taken 4 times.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 101 → 102 taken 4 times.
✓ Branch 102 → 103 taken 4 times.
|
4 | sieveObj["mode"] = config.sieve.mode; |
| 68 |
2/2✓ Branch 107 → 108 taken 1 time.
✓ Branch 107 → 127 taken 3 times.
|
4 | if (config.sieve.mode == QLatin1String("manual")) { |
| 69 |
4/4✓ Branch 108 → 109 taken 1 time.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 110 → 111 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
|
1 | sieveObj["host"] = config.sieve.host; |
| 70 |
4/4✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
✓ Branch 116 → 117 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
|
1 | sieveObj["port"] = config.sieve.port; |
| 71 |
4/4✓ Branch 120 → 121 taken 1 time.
✓ Branch 121 → 122 taken 1 time.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 123 → 124 taken 1 time.
|
1 | sieveObj["security"] = config.sieve.security; |
| 72 | } | ||
| 73 |
4/4✓ Branch 127 → 128 taken 4 times.
✓ Branch 128 → 129 taken 4 times.
✓ Branch 129 → 130 taken 4 times.
✓ Branch 130 → 131 taken 4 times.
|
4 | sieveObj["login"] = config.sieve.login; |
| 74 |
2/2✓ Branch 134 → 135 taken 2 times.
✓ Branch 134 → 150 taken 2 times.
|
4 | if (config.sieve.usesSeparateLogin()) { |
| 75 |
4/4✓ Branch 135 → 136 taken 2 times.
✓ Branch 136 → 137 taken 2 times.
✓ Branch 137 → 138 taken 2 times.
✓ Branch 138 → 139 taken 2 times.
|
2 | sieveObj["username"] = config.sieve.username; |
| 76 |
1/2✓ Branch 142 → 143 taken 2 times.
✗ Branch 142 → 150 not taken.
|
2 | if (!config.sieve.credentialId.isEmpty()) |
| 77 |
4/4✓ Branch 143 → 144 taken 2 times.
✓ Branch 144 → 145 taken 2 times.
✓ Branch 145 → 146 taken 2 times.
✓ Branch 146 → 147 taken 2 times.
|
2 | sieveObj["credentialId"] = config.sieve.credentialId; |
| 78 | } | ||
| 79 |
4/4✓ Branch 150 → 151 taken 4 times.
✓ Branch 151 → 152 taken 4 times.
✓ Branch 152 → 153 taken 4 times.
✓ Branch 153 → 154 taken 4 times.
|
4 | root["sieve"] = sieveObj; |
| 80 | 4 | } | |
| 81 |
4/4✓ Branch 158 → 159 taken 54 times.
✓ Branch 159 → 160 taken 54 times.
✓ Branch 160 → 161 taken 54 times.
✓ Branch 161 → 162 taken 54 times.
|
54 | root["id"] = config.id; |
| 82 |
4/4✓ Branch 164 → 165 taken 54 times.
✓ Branch 165 → 166 taken 54 times.
✓ Branch 166 → 167 taken 54 times.
✓ Branch 167 → 168 taken 54 times.
|
54 | root["name"] = config.name; |
| 83 |
4/4✓ Branch 170 → 171 taken 54 times.
✓ Branch 171 → 172 taken 54 times.
✓ Branch 172 → 173 taken 54 times.
✓ Branch 173 → 174 taken 54 times.
|
54 | root["email"] = config.email; |
| 84 |
2/2✓ Branch 177 → 178 taken 5 times.
✓ Branch 177 → 185 taken 49 times.
|
54 | if (!config.legacyKeyringName.isEmpty()) |
| 85 |
4/4✓ Branch 178 → 179 taken 5 times.
✓ Branch 179 → 180 taken 5 times.
✓ Branch 180 → 181 taken 5 times.
✓ Branch 181 → 182 taken 5 times.
|
5 | root["legacyKeyringName"] = config.legacyKeyringName; |
| 86 |
4/4✓ Branch 185 → 186 taken 54 times.
✓ Branch 186 → 187 taken 54 times.
✓ Branch 187 → 188 taken 54 times.
✓ Branch 188 → 189 taken 54 times.
|
54 | root["imap"] = imapObj; |
| 87 |
4/4✓ Branch 191 → 192 taken 54 times.
✓ Branch 192 → 193 taken 54 times.
✓ Branch 193 → 194 taken 54 times.
✓ Branch 194 → 195 taken 54 times.
|
54 | root["smtp"] = smtpObj; |
| 88 |
2/2✓ Branch 197 → 198 taken 54 times.
✓ Branch 198 → 199 taken 54 times.
|
108 | return QJsonDocument(root).toJson(QJsonDocument::Indented); |
| 89 | 54 | } | |
| 90 | |||
| 91 | // S106-203: a port outside 1..65535 reads as 0 (refused by validation) | ||
| 92 | // instead of wrapping into another valid port (65600 became 64). A missing | ||
| 93 | // or non-numeric value keeps the protocol default, as before. | ||
| 94 | 2159 | quint16 portValue(const QJsonValue &value, int fallback) { | |
| 95 |
2/2✓ Branch 3 → 4 taken 44 times.
✓ Branch 3 → 5 taken 2115 times.
|
2159 | if (!value.isDouble()) return quint16(fallback); |
| 96 | 2115 | const double port = value.toDouble(); | |
| 97 |
6/6✓ Branch 6 → 7 taken 2114 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 7 → 8 taken 2113 times.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 2112 times.
|
2115 | if (port < 1 || port > 65535 || port != double(qint64(port))) return 0; |
| 98 | 2112 | return quint16(port); | |
| 99 | } | ||
| 100 | |||
| 101 | 16 | bool readSecretBlocking(const QString &service, const QString &accountName, | |
| 102 | QByteArray *password) { | ||
| 103 |
1/1✓ Branch 2 → 3 taken 16 times.
|
16 | CredentialStore store; |
| 104 |
1/1✓ Branch 3 → 4 taken 16 times.
|
16 | QEventLoop loop; |
| 105 |
1/1✓ Branch 4 → 5 taken 16 times.
|
16 | QTimer timeout; |
| 106 |
1/1✓ Branch 5 → 6 taken 16 times.
|
16 | timeout.setSingleShot(true); |
| 107 | |||
| 108 | 16 | bool completed = false; | |
| 109 | 16 | bool success = false; | |
| 110 |
1/1✓ Branch 6 → 7 taken 16 times.
|
16 | QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() { |
| 111 | ✗ | if (completed) | |
| 112 | ✗ | return; | |
| 113 | ✗ | completed = true; | |
| 114 | ✗ | loop.quit(); | |
| 115 | }); | ||
| 116 |
1/1✓ Branch 9 → 10 taken 16 times.
|
16 | store.readPassword(service, accountName, |
| 117 |
1/1✓ Branch 8 → 9 taken 16 times.
|
16 | [&](bool ok, const QByteArray &value) { |
| 118 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 16 times.
|
16 | if (completed) |
| 119 | ✗ | return; | |
| 120 | 16 | completed = true; | |
| 121 | 16 | success = ok; | |
| 122 |
3/4✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 7 taken 14 times.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
|
16 | if (ok && password) |
| 123 | 2 | *password = value; | |
| 124 | 16 | loop.quit(); | |
| 125 | }); | ||
| 126 | // S106-201: a refusal (or any answer) can arrive synchronously; quit() | ||
| 127 | // before exec() is lost, and the loop would then never end. | ||
| 128 |
1/2✓ Branch 11 → 12 taken 16 times.
✗ Branch 11 → 16 not taken.
|
16 | if (!completed) { |
| 129 |
1/1✓ Branch 12 → 13 taken 16 times.
|
16 | timeout.start(30000); |
| 130 |
1/1✓ Branch 14 → 15 taken 16 times.
|
16 | loop.exec(); |
| 131 | } | ||
| 132 | 16 | return success; | |
| 133 | 16 | } | |
| 134 | |||
| 135 | 97 | bool writeSecretBlocking(const QString &service, const QString &accountName, | |
| 136 | const QByteArray &password, QString *error) { | ||
| 137 |
2/2✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 5 taken 37 times.
|
97 | if (password.isEmpty()) |
| 138 | 60 | return true; | |
| 139 | |||
| 140 |
1/1✓ Branch 5 → 6 taken 37 times.
|
37 | CredentialStore store; |
| 141 |
1/1✓ Branch 6 → 7 taken 37 times.
|
37 | QEventLoop loop; |
| 142 |
1/1✓ Branch 7 → 8 taken 37 times.
|
37 | QTimer timeout; |
| 143 |
1/1✓ Branch 8 → 9 taken 37 times.
|
37 | timeout.setSingleShot(true); |
| 144 | |||
| 145 | 37 | bool completed = false; | |
| 146 | 37 | bool success = false; | |
| 147 | 37 | QString writeError; | |
| 148 |
1/1✓ Branch 9 → 10 taken 37 times.
|
37 | QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() { |
| 149 | ✗ | if (completed) | |
| 150 | ✗ | return; | |
| 151 | ✗ | completed = true; | |
| 152 | ✗ | writeError = QStringLiteral("Timed out writing password to keyring"); | |
| 153 | ✗ | loop.quit(); | |
| 154 | }); | ||
| 155 |
1/1✓ Branch 12 → 13 taken 37 times.
|
37 | store.writePassword(service, accountName, password, |
| 156 |
1/1✓ Branch 11 → 12 taken 37 times.
|
37 | [&](bool ok, const QString &message) { |
| 157 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 37 times.
|
37 | if (completed) |
| 158 | ✗ | return; | |
| 159 | 37 | completed = true; | |
| 160 | 37 | success = ok; | |
| 161 | 37 | writeError = message; | |
| 162 | 37 | loop.quit(); | |
| 163 | }); | ||
| 164 | // S106-201: a refusal (or any answer) can arrive synchronously; quit() | ||
| 165 | // before exec() is lost, and the loop would then never end. | ||
| 166 |
1/2✓ Branch 14 → 15 taken 37 times.
✗ Branch 14 → 19 not taken.
|
37 | if (!completed) { |
| 167 |
1/1✓ Branch 15 → 16 taken 37 times.
|
37 | timeout.start(30000); |
| 168 |
1/1✓ Branch 17 → 18 taken 37 times.
|
37 | loop.exec(); |
| 169 | } | ||
| 170 | |||
| 171 |
3/4✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 32 taken 36 times.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 32 not taken.
|
37 | if (!success && error) { |
| 172 | 1 | *error = writeError.isEmpty() | |
| 173 |
2/4✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 1 time.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 1 time.
|
2 | ? QStringLiteral("Could not write password to keyring") |
| 174 | 1 | : writeError; | |
| 175 | } | ||
| 176 | 37 | return success; | |
| 177 | 37 | } | |
| 178 | |||
| 179 | |||
| 180 | 12 | bool deleteSecretBlocking(const QString &service, const QString &accountName, | |
| 181 | QString *error) { | ||
| 182 |
1/1✓ Branch 2 → 3 taken 12 times.
|
12 | CredentialStore store; |
| 183 |
1/1✓ Branch 3 → 4 taken 12 times.
|
12 | QEventLoop loop; |
| 184 |
1/1✓ Branch 4 → 5 taken 12 times.
|
12 | QTimer timeout; |
| 185 |
1/1✓ Branch 5 → 6 taken 12 times.
|
12 | timeout.setSingleShot(true); |
| 186 | |||
| 187 | 12 | bool completed = false; | |
| 188 | 12 | bool success = false; | |
| 189 | |||
| 190 |
1/1✓ Branch 6 → 7 taken 12 times.
|
12 | QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() { |
| 191 | ✗ | if (completed) | |
| 192 | ✗ | return; | |
| 193 | ✗ | completed = true; | |
| 194 | ✗ | if (error) | |
| 195 | ✗ | *error = QStringLiteral("Timed out deleting password from keyring"); | |
| 196 | ✗ | loop.quit(); | |
| 197 | }); | ||
| 198 | |||
| 199 |
2/2✓ Branch 8 → 9 taken 12 times.
✓ Branch 9 → 10 taken 12 times.
|
12 | store.deletePassword(service, accountName, [&](bool ok) { |
| 200 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 12 times.
|
12 | if (completed) |
| 201 | ✗ | return; | |
| 202 | 12 | completed = true; | |
| 203 | 12 | success = ok; | |
| 204 | 12 | loop.quit(); | |
| 205 | }); | ||
| 206 | |||
| 207 | // S106-201: a refusal (or any answer) can arrive synchronously; quit() | ||
| 208 | // before exec() is lost, and the loop would then never end. | ||
| 209 |
1/2✓ Branch 11 → 12 taken 12 times.
✗ Branch 11 → 16 not taken.
|
12 | if (!completed) { |
| 210 |
1/1✓ Branch 12 → 13 taken 12 times.
|
12 | timeout.start(30000); |
| 211 |
1/1✓ Branch 14 → 15 taken 12 times.
|
12 | loop.exec(); |
| 212 | } | ||
| 213 | |||
| 214 |
6/8✓ Branch 16 → 17 taken 10 times.
✓ Branch 16 → 21 taken 2 times.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 17 → 21 not taken.
✓ Branch 19 → 20 taken 10 times.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 10 times.
✓ Branch 22 → 30 taken 2 times.
|
12 | if (!success && error && error->isEmpty()) |
| 215 | 10 | *error = QStringLiteral("Could not delete password from keyring"); | |
| 216 | 12 | return success; | |
| 217 | 12 | } | |
| 218 | |||
| 219 | } // namespace | ||
| 220 | |||
| 221 | class AccountAsyncSave final : public QObject { | ||
| 222 | 2 | Q_DECLARE_TR_FUNCTIONS(AccountAsyncSave) | |
| 223 | public: | ||
| 224 | 10 | AccountAsyncSave(QObject *owner, AccountConfig config, QString directory, | |
| 225 | AccountConfigLoader::AsyncSaveCallback callback) | ||
| 226 | 20 | : QObject(owner), m_config(std::move(config)), m_directory(std::move(directory)), | |
| 227 |
5/8✓ Branch 5 → 6 taken 10 times.
✓ Branch 13 → 14 taken 10 times.
✓ Branch 14 → 15 taken 10 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10 times.
✓ Branch 17 → 18 taken 10 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
|
30 | m_callback(std::move(callback)), m_store(new CredentialStore(this)) { |
| 228 |
1/1✓ Branch 19 → 20 taken 10 times.
|
10 | m_deadline.setSingleShot(true); |
| 229 |
1/1✓ Branch 20 → 21 taken 10 times.
|
10 | m_deadline.setInterval(30000); |
| 230 | 10 | connect(&m_deadline, &QTimer::timeout, this, | |
| 231 |
3/3✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 21 → 22 taken 10 times.
|
11 | [this] { fail(tr("Timed out while saving credentials.")); }); |
| 232 |
1/1✓ Branch 23 → 24 taken 10 times.
|
18 | QTimer::singleShot(0, this, [this] { begin(); }); |
| 233 | 10 | } | |
| 234 | |||
| 235 | 20 | ~AccountAsyncSave() override { | |
| 236 |
4/6✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 6 taken 8 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 13 taken 10 times.
|
10 | if (!m_finished && !m_staged.isEmpty()) |
| 237 | ✗ | CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), m_staged, {}); | |
| 238 | 20 | } | |
| 239 | |||
| 240 | private: | ||
| 241 | 8 | void begin() { | |
| 242 |
1/1✓ Branch 2 → 3 taken 8 times.
|
8 | QDir directory(m_directory); |
| 243 |
5/16✓ Branch 3 → 4 taken 8 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 11 taken 8 times.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 9 → 11 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 8 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 8 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 8 times.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 215 not taken.
✗ Branch 217 → 218 not taken.
✗ Branch 217 → 219 not taken.
|
8 | if (!directory.exists() && !directory.mkpath(QStringLiteral("."))) { |
| 244 | ✗ | fail(tr("The account directory could not be created.")); | |
| 245 | ✗ | return; | |
| 246 | } | ||
| 247 |
2/2✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 28 taken 7 times.
|
8 | if (m_config.id.isEmpty()) |
| 248 |
2/2✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
|
1 | m_config.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 249 |
1/1✓ Branch 28 → 29 taken 8 times.
|
8 | const auto errors = AccountConfigLoader::validate(m_config); |
| 250 |
1/4✗ Branch 30 → 31 not taken.
✓ Branch 30 → 40 taken 8 times.
✗ Branch 34 → 35 not taken.
✗ Branch 35 → 36 not taken.
|
8 | if (!errors.isEmpty()) { fail(errors.join(QStringLiteral("; "))); return; } |
| 251 |
1/1✓ Branch 40 → 41 taken 8 times.
|
8 | const auto uniqueness = AccountConfigLoader::checkNameUniqueness(m_config, m_directory); |
| 252 |
1/3✗ Branch 42 → 43 not taken.
✓ Branch 42 → 47 taken 8 times.
✗ Branch 44 → 45 not taken.
|
8 | if (!uniqueness.isEmpty()) { fail(uniqueness); return; } |
| 253 | |||
| 254 |
3/5✓ Branch 53 → 54 taken 8 times.
✓ Branch 56 → 57 taken 8 times.
✓ Branch 56 → 58 taken 8 times.
✗ Branch 241 → 242 not taken.
✗ Branch 241 → 243 not taken.
|
24 | const auto files = directory.entryList({QStringLiteral("*.json")}, QDir::Files, QDir::Name); |
| 255 |
2/2✓ Branch 111 → 61 taken 8 times.
✓ Branch 111 → 112 taken 8 times.
|
16 | for (const auto &fileName : files) { |
| 256 |
1/1✓ Branch 62 → 63 taken 8 times.
|
8 | const QString path = directory.absoluteFilePath(fileName); |
| 257 | auto existing = AccountConfigLoader::loadFromFile( | ||
| 258 |
1/1✓ Branch 63 → 64 taken 8 times.
|
8 | path, AccountConfigLoader::LoadMode::MetadataOnly); |
| 259 |
5/6✓ Branch 65 → 66 taken 8 times.
✗ Branch 65 → 69 not taken.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 70 taken 7 times.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 7 times.
|
8 | if (!existing || existing->id != m_config.id) continue; |
| 260 |
1/2✗ Branch 74 → 75 not taken.
✓ Branch 74 → 79 taken 7 times.
|
7 | if (!m_sourcePath.isEmpty()) { |
| 261 | ✗ | fail(tr("The account identity is stored more than once.")); | |
| 262 | ✗ | return; | |
| 263 | } | ||
| 264 |
1/1✓ Branch 79 → 80 taken 7 times.
|
7 | QFile source(path); |
| 265 |
2/5✓ Branch 81 → 82 taken 7 times.
✗ Branch 82 → 83 not taken.
✓ Branch 82 → 87 taken 7 times.
✗ Branch 83 → 84 not taken.
✗ Branch 84 → 85 not taken.
|
7 | if (!source.open(QIODevice::ReadOnly)) { fail(source.errorString()); return; } |
| 266 |
1/1✓ Branch 87 → 88 taken 7 times.
|
7 | m_sourceBytes = source.readAll(); |
| 267 | 7 | m_sourcePath = path; | |
| 268 | 7 | m_previous = *existing; | |
| 269 | 9 | } | |
| 270 | |||
| 271 |
1/1✓ Branch 117 → 118 taken 8 times.
|
16 | m_targetPath = directory.absoluteFilePath( |
| 272 |
2/2✓ Branch 115 → 116 taken 8 times.
✓ Branch 116 → 117 taken 8 times.
|
24 | AccountConfigLoader::fileStem(m_config, m_directory) + QStringLiteral(".json")); |
| 273 |
5/7✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 129 taken 7 times.
✓ Branch 126 → 127 taken 1 time.
✗ Branch 127 → 128 not taken.
✓ Branch 127 → 129 taken 1 time.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 135 taken 8 times.
|
8 | if (m_sourcePath.isEmpty() && QFileInfo::exists(m_targetPath)) { |
| 274 | ✗ | fail(tr("The account name is already in use.")); | |
| 275 | ✗ | return; | |
| 276 | } | ||
| 277 |
2/2✓ Branch 136 → 137 taken 7 times.
✓ Branch 136 → 154 taken 1 time.
|
8 | if (!m_sourcePath.isEmpty()) { |
| 278 |
2/2✓ Branch 138 → 139 taken 4 times.
✓ Branch 138 → 140 taken 3 times.
|
7 | if (m_config.imap.credentialId.isEmpty()) |
| 279 | 4 | m_config.imap.credentialId = m_previous.imap.credentialId; | |
| 280 |
2/2✓ Branch 141 → 142 taken 4 times.
✓ Branch 141 → 143 taken 3 times.
|
7 | if (m_config.smtp.credentialId.isEmpty()) |
| 281 | 4 | m_config.smtp.credentialId = m_previous.smtp.credentialId; | |
| 282 |
6/6✓ Branch 144 → 145 taken 5 times.
✓ Branch 144 → 148 taken 2 times.
✓ Branch 146 → 147 taken 1 time.
✓ Branch 146 → 148 taken 4 times.
✓ Branch 149 → 150 taken 1 time.
✓ Branch 149 → 151 taken 6 times.
|
7 | if (m_config.sieve.credentialId.isEmpty() && m_config.sieve.usesSeparateLogin()) |
| 283 | 1 | m_config.sieve.credentialId = m_previous.sieve.credentialId; | |
| 284 |
1/2✓ Branch 152 → 153 taken 7 times.
✗ Branch 152 → 154 not taken.
|
7 | if (m_config.legacyKeyringName.isEmpty()) |
| 285 | 7 | m_config.legacyKeyringName = m_previous.legacyKeyringName; | |
| 286 | } | ||
| 287 | |||
| 288 | 8 | const QByteArray imapPassword = !m_config.imap.password.isEmpty() | |
| 289 |
2/2✓ Branch 155 → 156 taken 4 times.
✓ Branch 155 → 157 taken 4 times.
|
8 | ? m_config.imap.password : m_previous.imap.password; |
| 290 | 8 | const QByteArray smtpPassword = !m_config.smtp.password.isEmpty() | |
| 291 |
2/2✓ Branch 160 → 161 taken 4 times.
✓ Branch 160 → 162 taken 4 times.
|
8 | ? m_config.smtp.password : m_previous.smtp.password; |
| 292 |
1/1✓ Branch 167 → 168 taken 8 times.
|
8 | stage(QStringLiteral("imap"), imapPassword, |
| 293 | 8 | m_config.imap.credentialId); | |
| 294 |
1/1✓ Branch 173 → 174 taken 8 times.
|
8 | stage(QStringLiteral("smtp"), smtpPassword, |
| 295 | 8 | m_config.smtp.credentialId); | |
| 296 |
2/2✓ Branch 177 → 178 taken 2 times.
✓ Branch 177 → 185 taken 6 times.
|
8 | if (m_config.sieve.usesSeparateLogin()) |
| 297 |
1/1✓ Branch 181 → 182 taken 2 times.
|
4 | stage(QStringLiteral("sieve"), m_config.sieve.password, m_config.sieve.credentialId); |
| 298 | else | ||
| 299 | 6 | m_config.sieve.credentialId.clear(); | |
| 300 |
1/1✓ Branch 186 → 187 taken 8 times.
|
8 | m_config.imap.password.clear(); |
| 301 |
1/1✓ Branch 187 → 188 taken 8 times.
|
8 | m_config.smtp.password.clear(); |
| 302 |
1/1✓ Branch 188 → 189 taken 8 times.
|
8 | m_config.sieve.password.clear(); |
| 303 |
3/3✓ Branch 189 → 190 taken 2 times.
✓ Branch 189 → 191 taken 6 times.
✓ Branch 190 → 192 taken 2 times.
|
8 | if (m_pending == 0) commit(); |
| 304 |
1/1✓ Branch 191 → 192 taken 6 times.
|
6 | else m_deadline.start(); |
| 305 | 24 | } | |
| 306 | |||
| 307 | 18 | void stage(const QString &service, const QByteArray &password, | |
| 308 | QString &credentialId) { | ||
| 309 |
2/2✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 12 times.
|
18 | if (password.isEmpty()) return; |
| 310 |
2/2✓ Branch 5 → 6 taken 12 times.
✓ Branch 6 → 7 taken 12 times.
|
12 | credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 311 | 12 | const CredentialStore::SecretKey key{service, credentialId}; | |
| 312 |
1/1✓ Branch 11 → 12 taken 12 times.
|
12 | m_staged.append(key); |
| 313 | 12 | ++m_pending; | |
| 314 |
1/1✓ Branch 13 → 14 taken 12 times.
|
12 | m_store->writePassword(service, credentialId, password, |
| 315 | 12 | [this](bool success, const QString &error) { | |
| 316 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 10 times.
|
10 | if (m_finished) return; |
| 317 |
1/6✗ Branch 4 → 5 not taken.
✓ Branch 4 → 12 taken 10 times.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ Branch 9 → 10 not taken.
|
10 | if (!success) { fail(error.isEmpty() ? tr("The password could not be stored.") : error); return; } |
| 318 |
2/2✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 5 times.
|
10 | if (--m_pending == 0) commit(); |
| 319 | }); | ||
| 320 | 12 | } | |
| 321 | |||
| 322 | 7 | void commit() { | |
| 323 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 7 times.
|
8 | if (m_finished) return; |
| 324 |
1/1✓ Branch 4 → 5 taken 7 times.
|
7 | m_deadline.stop(); |
| 325 |
1/2✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 29 not taken.
|
7 | if (!m_sourcePath.isEmpty()) { |
| 326 |
1/1✓ Branch 7 → 8 taken 7 times.
|
7 | QFile source(m_sourcePath); |
| 327 |
8/12✓ Branch 9 → 10 taken 7 times.
✓ Branch 10 → 11 taken 7 times.
✗ Branch 10 → 14 not taken.
✓ Branch 11 → 12 taken 7 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 6 times.
✓ Branch 16 → 17 taken 7 times.
✗ Branch 16 → 18 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 23 taken 6 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
|
7 | if (!source.open(QIODevice::ReadOnly) || source.readAll() != m_sourceBytes) { |
| 328 |
2/2✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
|
1 | fail(tr("The account changed while it was being saved. Review the current values and try again.")); |
| 329 | 1 | return; | |
| 330 | } | ||
| 331 |
2/5✓ Branch 25 → 26 taken 6 times.
✓ Branch 25 → 28 taken 1 time.
✗ Branch 29 → 30 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 35 not taken.
|
7 | } else if (QFileInfo::exists(m_targetPath)) { |
| 332 | ✗ | fail(tr("The account changed while it was being saved. Review the current values and try again.")); | |
| 333 | ✗ | return; | |
| 334 | } | ||
| 335 |
1/1✓ Branch 35 → 36 taken 6 times.
|
6 | const auto uniqueness = AccountConfigLoader::checkNameUniqueness(m_config, m_directory); |
| 336 |
1/3✗ Branch 37 → 38 not taken.
✓ Branch 37 → 42 taken 6 times.
✗ Branch 39 → 40 not taken.
|
6 | if (!uniqueness.isEmpty()) { fail(uniqueness); return; } |
| 337 |
5/7✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 47 taken 4 times.
✓ Branch 44 → 45 taken 2 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 2 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 53 taken 6 times.
|
6 | if (m_targetPath != m_sourcePath && QFileInfo::exists(m_targetPath)) { |
| 338 | ✗ | fail(tr("The account name is already in use.")); | |
| 339 | ✗ | return; | |
| 340 | } | ||
| 341 |
3/4✓ Branch 53 → 54 taken 6 times.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 61 taken 6 times.
|
6 | if (!writeAccountBytes(m_targetPath, accountMetadata(m_config))) { |
| 342 | ✗ | fail(tr("The account metadata could not be saved.")); | |
| 343 | ✗ | return; | |
| 344 | } | ||
| 345 |
4/6✓ Branch 62 → 63 taken 6 times.
✗ Branch 62 → 68 not taken.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 68 taken 4 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 75 taken 6 times.
|
8 | if (!m_sourcePath.isEmpty() && m_sourcePath != m_targetPath && |
| 346 |
2/3✓ Branch 65 → 66 taken 2 times.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 68 taken 2 times.
|
2 | !QFile::remove(m_sourcePath)) { |
| 347 | ✗ | QFile::remove(m_targetPath); | |
| 348 | ✗ | fail(tr("The previous account file could not be replaced.")); | |
| 349 | ✗ | return; | |
| 350 | } | ||
| 351 | |||
| 352 | 6 | QList<CredentialStore::SecretKey> obsolete; | |
| 353 |
1/2✓ Branch 76 → 77 taken 6 times.
✗ Branch 76 → 138 not taken.
|
6 | if (!m_sourcePath.isEmpty()) { |
| 354 | 6 | const QString oldImap = AccountConfigLoader::imapCredentialName(m_previous); | |
| 355 | 6 | const QString oldSmtp = AccountConfigLoader::smtpCredentialName(m_previous); | |
| 356 |
6/8✓ Branch 80 → 81 taken 6 times.
✗ Branch 80 → 85 not taken.
✓ Branch 83 → 84 taken 3 times.
✓ Branch 83 → 85 taken 3 times.
✓ Branch 86 → 87 taken 6 times.
✗ Branch 86 → 88 not taken.
✓ Branch 88 → 89 taken 3 times.
✓ Branch 88 → 97 taken 3 times.
|
6 | if (!oldImap.isEmpty() && oldImap != AccountConfigLoader::imapCredentialName(m_config)) |
| 357 | 3 | obsolete.append({QStringLiteral("imap"), oldImap}); | |
| 358 |
6/8✓ Branch 98 → 99 taken 6 times.
✗ Branch 98 → 103 not taken.
✓ Branch 101 → 102 taken 3 times.
✓ Branch 101 → 103 taken 3 times.
✓ Branch 104 → 105 taken 6 times.
✗ Branch 104 → 106 not taken.
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 115 taken 3 times.
|
6 | if (!oldSmtp.isEmpty() && oldSmtp != AccountConfigLoader::smtpCredentialName(m_config)) |
| 359 | 3 | obsolete.append({QStringLiteral("smtp"), oldSmtp}); | |
| 360 | 6 | const QString oldSieve = AccountConfigLoader::sieveCredentialName(m_previous); | |
| 361 |
7/8✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 122 taken 5 times.
✓ Branch 120 → 121 taken 1 time.
✗ Branch 120 → 122 not taken.
✓ Branch 123 → 124 taken 1 time.
✓ Branch 123 → 125 taken 5 times.
✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 134 taken 5 times.
|
6 | if (!oldSieve.isEmpty() && oldSieve != AccountConfigLoader::sieveCredentialName(m_config)) |
| 362 | 1 | obsolete.append({QStringLiteral("sieve"), oldSieve}); | |
| 363 | 6 | } | |
| 364 | 6 | m_finished = true; | |
| 365 |
1/1✓ Branch 138 → 139 taken 6 times.
|
6 | m_staged.clear(); |
| 366 | 6 | const auto callback = std::move(m_callback); | |
| 367 |
2/2✓ Branch 142 → 143 taken 4 times.
✓ Branch 142 → 148 taken 2 times.
|
6 | if (!obsolete.isEmpty()) |
| 368 |
1/1✓ Branch 145 → 146 taken 4 times.
|
4 | CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), obsolete, {}); |
| 369 |
2/3✓ Branch 149 → 150 taken 6 times.
✗ Branch 149 → 154 not taken.
✓ Branch 151 → 152 taken 6 times.
|
6 | if (callback) callback(true, m_config, {}); |
| 370 |
1/1✓ Branch 154 → 155 taken 6 times.
|
6 | deleteLater(); |
| 371 | 13 | } | |
| 372 | |||
| 373 | 2 | void fail(QString error) { | |
| 374 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
|
2 | if (m_finished) return; |
| 375 | 2 | m_finished = true; | |
| 376 |
1/1✓ Branch 4 → 5 taken 2 times.
|
2 | m_deadline.stop(); |
| 377 | // A failed keyring write reports from inside the finished signal of a | ||
| 378 | // job owned by m_store; deleting the store synchronously freed that job | ||
| 379 | // while it was still emitting (use-after-free, crash on a later timer; | ||
| 380 | // Sprint 100 SB-14). Remaining jobs are dropped with the store after | ||
| 381 | // the current event; their callbacks see m_finished and return. | ||
| 382 |
2/3✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 2 times.
|
2 | if (m_store) m_store->deleteLater(); |
| 383 | 2 | m_store = nullptr; | |
| 384 | 2 | const auto staged = std::exchange(m_staged, {}); | |
| 385 | 2 | const auto callback = std::move(m_callback); | |
| 386 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 19 taken 2 times.
|
2 | if (staged.isEmpty()) { |
| 387 | ✗ | if (callback) callback(false, m_config, error); | |
| 388 | ✗ | deleteLater(); | |
| 389 | ✗ | return; | |
| 390 | } | ||
| 391 |
1/1✓ Branch 23 → 24 taken 2 times.
|
2 | CredentialStore::deletePasswordsAsync(this, staged, |
| 392 |
2/4✓ Branch 19 → 20 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
|
4 | [this, callback, error = std::move(error)](bool) { |
| 393 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
|
2 | if (callback) callback(false, m_config, error); |
| 394 | 2 | deleteLater(); | |
| 395 | 2 | }); | |
| 396 | 2 | } | |
| 397 | |||
| 398 | AccountConfig m_config; | ||
| 399 | AccountConfig m_previous; | ||
| 400 | QString m_directory; | ||
| 401 | QString m_sourcePath; | ||
| 402 | QString m_targetPath; | ||
| 403 | QByteArray m_sourceBytes; | ||
| 404 | AccountConfigLoader::AsyncSaveCallback m_callback; | ||
| 405 | CredentialStore *m_store; | ||
| 406 | QTimer m_deadline; | ||
| 407 | QList<CredentialStore::SecretKey> m_staged; | ||
| 408 | int m_pending = 0; | ||
| 409 | bool m_finished = false; | ||
| 410 | }; | ||
| 411 | |||
| 412 | 10 | QObject *AccountConfigLoader::saveAsync(QObject *owner, AccountConfig config, | |
| 413 | const QString &configDir, | ||
| 414 | AsyncSaveCallback callback) { | ||
| 415 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 10 times.
|
10 | if (!owner) return nullptr; |
| 416 | 10 | return new AccountAsyncSave(owner, std::move(config), configDir, | |
| 417 |
2/5✓ Branch 10 → 11 taken 10 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
|
20 | std::move(callback)); |
| 418 | } | ||
| 419 | |||
| 420 | 114 | QString AccountConfigLoader::imapCredentialName(const AccountConfig &account) { | |
| 421 |
2/2✓ Branch 3 → 4 taken 107 times.
✓ Branch 3 → 5 taken 7 times.
|
114 | return account.imap.credentialId.isEmpty() ? legacyCredentialName(account) |
| 422 | 114 | : account.imap.credentialId; | |
| 423 | } | ||
| 424 | |||
| 425 | 94 | QString AccountConfigLoader::smtpCredentialName(const AccountConfig &account) { | |
| 426 |
2/2✓ Branch 3 → 4 taken 87 times.
✓ Branch 3 → 5 taken 7 times.
|
94 | return account.smtp.credentialId.isEmpty() ? legacyCredentialName(account) |
| 427 | 94 | : account.smtp.credentialId; | |
| 428 | } | ||
| 429 | |||
| 430 | 8 | QString AccountConfigLoader::sieveCredentialName(const AccountConfig &account) { | |
| 431 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 6 times.
|
8 | return account.sieve.usesSeparateLogin() ? account.sieve.credentialId : QString(); |
| 432 | } | ||
| 433 | |||
| 434 | 1 | QString AccountConfigLoader::defaultConfigDir() { | |
| 435 |
1/1✓ Branch 2 → 3 taken 1 time.
|
2 | return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + |
| 436 |
1/1✓ Branch 3 → 4 taken 1 time.
|
2 | "/mailjd/accounts"; |
| 437 | } | ||
| 438 | |||
| 439 | ✗ | std::vector<AccountConfig> AccountConfigLoader::loadAll() { | |
| 440 | ✗ | return loadAll(defaultConfigDir()); | |
| 441 | } | ||
| 442 | |||
| 443 | std::vector<AccountConfig> | ||
| 444 | 884 | AccountConfigLoader::loadAll(const QString &configDir, LoadMode mode) { | |
| 445 | 884 | std::vector<AccountConfig> accounts; | |
| 446 |
1/1✓ Branch 2 → 3 taken 884 times.
|
884 | QDir dir(configDir); |
| 447 | |||
| 448 |
3/3✓ Branch 3 → 4 taken 884 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 17 taken 881 times.
|
884 | if (!dir.exists()) { |
| 449 |
4/4✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 15 → 7 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
|
6 | qCWarning(lcAccountConfig) |
| 450 |
2/2✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
|
3 | << "Config directory does not exist:" << configDir; |
| 451 | 3 | return accounts; | |
| 452 | } | ||
| 453 | |||
| 454 |
3/5✓ Branch 21 → 22 taken 881 times.
✓ Branch 24 → 25 taken 881 times.
✓ Branch 24 → 26 taken 881 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
|
2643 | const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name); |
| 455 |
2/2✓ Branch 78 → 28 taken 925 times.
✓ Branch 78 → 79 taken 881 times.
|
1806 | for (const auto &fileName : files) { |
| 456 |
1/1✓ Branch 29 → 30 taken 925 times.
|
925 | const auto path = dir.absoluteFilePath(fileName); |
| 457 |
1/1✓ Branch 30 → 31 taken 925 times.
|
925 | auto account = loadFromFile(path, mode); |
| 458 |
2/2✓ Branch 32 → 33 taken 924 times.
✓ Branch 32 → 74 taken 1 time.
|
925 | if (account.has_value()) { |
| 459 |
2/2✓ Branch 33 → 34 taken 924 times.
✓ Branch 34 → 35 taken 924 times.
|
924 | auto errors = validate(account.value()); |
| 460 |
2/2✓ Branch 36 → 37 taken 921 times.
✓ Branch 36 → 54 taken 3 times.
|
924 | if (errors.isEmpty()) { |
| 461 | // The id, not the name: names are usually the mail address | ||
| 462 | // (S102.4.6, no address in the log at info level). | ||
| 463 |
4/4✓ Branch 37 → 38 taken 921 times.
✓ Branch 41 → 42 taken 921 times.
✓ Branch 50 → 39 taken 921 times.
✓ Branch 50 → 51 taken 921 times.
|
1842 | qCInfo(lcAccountConfig) |
| 464 |
4/4✓ Branch 42 → 43 taken 921 times.
✓ Branch 44 → 45 taken 921 times.
✓ Branch 45 → 46 taken 921 times.
✓ Branch 46 → 47 taken 921 times.
|
921 | << "Loaded account:" << account->id << "from" << fileName; |
| 465 |
2/2✓ Branch 51 → 52 taken 921 times.
✓ Branch 53 → 72 taken 921 times.
|
921 | accounts.push_back(std::move(account.value())); |
| 466 | } else { | ||
| 467 |
4/4✓ Branch 54 → 55 taken 3 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 70 → 56 taken 3 times.
✓ Branch 70 → 71 taken 3 times.
|
6 | qCWarning(lcAccountConfig) |
| 468 |
6/6✓ Branch 59 → 60 taken 3 times.
✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
|
3 | << "Invalid account config" << fileName << ":" << errors.join(", "); |
| 469 | } | ||
| 470 | 924 | } | |
| 471 | 925 | } | |
| 472 | |||
| 473 |
7/7✓ Branch 79 → 80 taken 881 times.
✓ Branch 83 → 84 taken 881 times.
✓ Branch 84 → 85 taken 881 times.
✓ Branch 86 → 87 taken 881 times.
✓ Branch 87 → 88 taken 881 times.
✓ Branch 92 → 81 taken 881 times.
✓ Branch 92 → 93 taken 881 times.
|
1762 | qCInfo(lcAccountConfig) << "Loaded" << accounts.size() << "accounts from" |
| 474 |
1/1✓ Branch 88 → 89 taken 881 times.
|
881 | << configDir; |
| 475 | 881 | return accounts; | |
| 476 | 1765 | } | |
| 477 | |||
| 478 | std::optional<AccountConfig> | ||
| 479 | 1072 | AccountConfigLoader::loadFromFile(const QString &path, LoadMode mode) { | |
| 480 |
1/1✓ Branch 2 → 3 taken 1072 times.
|
1072 | QFile file(path); |
| 481 |
3/3✓ Branch 4 → 5 taken 1072 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 21 taken 1071 times.
|
1072 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { |
| 482 |
4/4✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 19 → 8 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
2 | qCWarning(lcAccountConfig) |
| 483 |
4/4✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
|
1 | << "Cannot open config file:" << path << file.errorString(); |
| 484 | 1 | return std::nullopt; | |
| 485 | } | ||
| 486 | |||
| 487 | 1071 | QJsonParseError parseError; | |
| 488 |
1/1✓ Branch 21 → 22 taken 1071 times.
|
1071 | QByteArray source = file.readAll(); |
| 489 |
1/1✓ Branch 22 → 23 taken 1071 times.
|
1071 | auto doc = QJsonDocument::fromJson(source, &parseError); |
| 490 |
3/3✓ Branch 23 → 24 taken 1071 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 41 taken 1067 times.
|
1071 | if (doc.isNull()) { |
| 491 |
4/4✓ Branch 25 → 26 taken 4 times.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 39 → 27 taken 4 times.
✓ Branch 39 → 40 taken 4 times.
|
8 | qCWarning(lcAccountConfig) |
| 492 |
5/5✓ Branch 30 → 31 taken 4 times.
✓ Branch 31 → 32 taken 4 times.
✓ Branch 32 → 33 taken 4 times.
✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
|
4 | << "JSON parse error in" << path << ":" << parseError.errorString(); |
| 493 | 4 | return std::nullopt; | |
| 494 | } | ||
| 495 | |||
| 496 |
3/3✓ Branch 41 → 42 taken 1067 times.
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 55 taken 1066 times.
|
1067 | if (!doc.isObject()) { |
| 497 |
6/6✓ Branch 43 → 44 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 53 → 45 taken 1 time.
✓ Branch 53 → 54 taken 1 time.
|
2 | qCWarning(lcAccountConfig) << "JSON root is not an object in" << path; |
| 498 | 1 | return std::nullopt; | |
| 499 | } | ||
| 500 | |||
| 501 |
1/1✓ Branch 55 → 56 taken 1066 times.
|
1066 | const auto root = doc.object(); |
| 502 |
1/1✓ Branch 56 → 57 taken 1066 times.
|
1066 | AccountConfig config; |
| 503 | |||
| 504 |
3/3✓ Branch 57 → 58 taken 1066 times.
✓ Branch 58 → 59 taken 1066 times.
✓ Branch 59 → 60 taken 1066 times.
|
1066 | config.id = root.value("id").toString(); |
| 505 |
6/6✓ Branch 65 → 66 taken 1048 times.
✓ Branch 65 → 69 taken 18 times.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 69 taken 1047 times.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 70 → 83 taken 1065 times.
|
1066 | if (!config.id.isEmpty() && !isSafeId(config.id)) { |
| 506 |
6/6✓ Branch 71 → 72 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 81 → 73 taken 1 time.
✓ Branch 81 → 82 taken 1 time.
|
2 | qCWarning(lcAccountConfig) << "Account file refused: its id is not a plain name" << path; |
| 507 | 1 | return std::nullopt; | |
| 508 | } | ||
| 509 |
3/3✓ Branch 83 → 84 taken 1065 times.
✓ Branch 84 → 85 taken 1065 times.
✓ Branch 85 → 86 taken 1065 times.
|
1065 | config.name = root.value("name").toString(); |
| 510 |
3/3✓ Branch 90 → 91 taken 1065 times.
✓ Branch 91 → 92 taken 1065 times.
✓ Branch 92 → 93 taken 1065 times.
|
1065 | config.email = root.value("email").toString(); |
| 511 |
3/3✓ Branch 97 → 98 taken 1065 times.
✓ Branch 98 → 99 taken 1065 times.
✓ Branch 99 → 100 taken 1065 times.
|
1065 | config.legacyKeyringName = root.value("legacyKeyringName").toString(); |
| 512 | 1065 | bool metadataNeedsRewrite = false; | |
| 513 |
2/2✓ Branch 105 → 106 taken 18 times.
✓ Branch 105 → 112 taken 1047 times.
|
1065 | if (config.id.isEmpty()) { |
| 514 |
2/2✓ Branch 106 → 107 taken 18 times.
✓ Branch 107 → 108 taken 18 times.
|
18 | config.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 515 | 18 | config.legacyKeyringName = config.name; | |
| 516 | 18 | metadataNeedsRewrite = true; | |
| 517 | } | ||
| 518 | |||
| 519 | // IMAP | ||
| 520 |
3/3✓ Branch 112 → 113 taken 1065 times.
✓ Branch 113 → 114 taken 1065 times.
✓ Branch 114 → 115 taken 1065 times.
|
1065 | const auto imapObj = root.value("imap").toObject(); |
| 521 |
3/3✓ Branch 117 → 118 taken 1065 times.
✓ Branch 118 → 119 taken 1065 times.
✓ Branch 119 → 120 taken 1065 times.
|
1065 | config.imap.host = imapObj.value("host").toString(); |
| 522 |
3/3✓ Branch 124 → 125 taken 1065 times.
✓ Branch 125 → 126 taken 1065 times.
✓ Branch 126 → 127 taken 1065 times.
|
1065 | config.imap.port = portValue(imapObj.value("port"), 993); |
| 523 |
4/4✓ Branch 129 → 130 taken 1065 times.
✓ Branch 130 → 131 taken 1065 times.
✓ Branch 131 → 132 taken 1065 times.
✓ Branch 132 → 133 taken 1065 times.
|
1065 | config.imap.security = imapObj.value("security").toString("ssl"); |
| 524 |
3/3✓ Branch 138 → 139 taken 1065 times.
✓ Branch 139 → 140 taken 1065 times.
✓ Branch 140 → 141 taken 1065 times.
|
1065 | config.imap.username = imapObj.value("username").toString(); |
| 525 |
4/4✓ Branch 145 → 146 taken 1065 times.
✓ Branch 146 → 147 taken 1065 times.
✓ Branch 147 → 148 taken 1065 times.
✓ Branch 148 → 149 taken 1065 times.
|
1065 | config.imap.password = imapObj.value("password").toString().toUtf8(); |
| 526 |
3/3✓ Branch 154 → 155 taken 1065 times.
✓ Branch 155 → 156 taken 1065 times.
✓ Branch 156 → 157 taken 1065 times.
|
1065 | config.imap.credentialId = imapObj.value("credentialId").toString(); |
| 527 | |||
| 528 | // SMTP | ||
| 529 |
3/3✓ Branch 161 → 162 taken 1065 times.
✓ Branch 162 → 163 taken 1065 times.
✓ Branch 163 → 164 taken 1065 times.
|
1065 | const auto smtpObj = root.value("smtp").toObject(); |
| 530 |
3/3✓ Branch 166 → 167 taken 1065 times.
✓ Branch 167 → 168 taken 1065 times.
✓ Branch 168 → 169 taken 1065 times.
|
1065 | config.smtp.host = smtpObj.value("host").toString(); |
| 531 |
3/3✓ Branch 173 → 174 taken 1065 times.
✓ Branch 174 → 175 taken 1065 times.
✓ Branch 175 → 176 taken 1065 times.
|
1065 | config.smtp.port = portValue(smtpObj.value("port"), 587); |
| 532 |
4/4✓ Branch 178 → 179 taken 1065 times.
✓ Branch 179 → 180 taken 1065 times.
✓ Branch 180 → 181 taken 1065 times.
✓ Branch 181 → 182 taken 1065 times.
|
1065 | config.smtp.security = smtpObj.value("security").toString("starttls"); |
| 533 |
3/3✓ Branch 187 → 188 taken 1065 times.
✓ Branch 188 → 189 taken 1065 times.
✓ Branch 189 → 190 taken 1065 times.
|
1065 | config.smtp.username = smtpObj.value("username").toString(); |
| 534 |
4/4✓ Branch 194 → 195 taken 1065 times.
✓ Branch 195 → 196 taken 1065 times.
✓ Branch 196 → 197 taken 1065 times.
✓ Branch 197 → 198 taken 1065 times.
|
1065 | config.smtp.password = smtpObj.value("password").toString().toUtf8(); |
| 535 |
3/3✓ Branch 203 → 204 taken 1065 times.
✓ Branch 204 → 205 taken 1065 times.
✓ Branch 205 → 206 taken 1065 times.
|
1065 | config.smtp.credentialId = smtpObj.value("credentialId").toString(); |
| 536 |
4/5✓ Branch 211 → 212 taken 1065 times.
✓ Branch 212 → 213 taken 1065 times.
✓ Branch 213 → 214 taken 1065 times.
✗ Branch 215 → 216 not taken.
✓ Branch 215 → 219 taken 1065 times.
|
4260 | config.smtp.login = smtpObj.value("login").toString() == QLatin1String("imap") ? QStringLiteral("imap") |
| 537 |
2/4✓ Branch 224 → 225 taken 1065 times.
✗ Branch 224 → 226 not taken.
✗ Branch 226 → 227 not taken.
✓ Branch 226 → 228 taken 1065 times.
|
2130 | : QStringLiteral("separate"); |
| 538 | |||
| 539 | // Sieve (absent = auto with the IMAP login) | ||
| 540 |
3/3✓ Branch 231 → 232 taken 1065 times.
✓ Branch 232 → 233 taken 1065 times.
✓ Branch 233 → 234 taken 1065 times.
|
1065 | const auto sieveObj = root.value("sieve").toObject(); |
| 541 |
3/3✓ Branch 236 → 237 taken 1065 times.
✓ Branch 237 → 238 taken 29 times.
✓ Branch 237 → 301 taken 1036 times.
|
1065 | if (!sieveObj.isEmpty()) { |
| 542 |
3/3✓ Branch 238 → 239 taken 29 times.
✓ Branch 239 → 240 taken 29 times.
✓ Branch 243 → 244 taken 29 times.
|
58 | config.sieve.mode = sieveObj.value("mode").toString(QStringLiteral("auto")); |
| 543 |
3/3✓ Branch 250 → 251 taken 29 times.
✓ Branch 251 → 252 taken 29 times.
✓ Branch 252 → 253 taken 29 times.
|
29 | config.sieve.host = sieveObj.value("host").toString(); |
| 544 |
3/3✓ Branch 257 → 258 taken 29 times.
✓ Branch 258 → 259 taken 29 times.
✓ Branch 259 → 260 taken 29 times.
|
29 | config.sieve.port = portValue(sieveObj.value("port"), 4190); |
| 545 |
3/3✓ Branch 262 → 263 taken 29 times.
✓ Branch 263 → 264 taken 29 times.
✓ Branch 267 → 268 taken 29 times.
|
58 | config.sieve.security = sieveObj.value("security").toString(QStringLiteral("starttls")); |
| 546 |
3/3✓ Branch 274 → 275 taken 29 times.
✓ Branch 275 → 276 taken 29 times.
✓ Branch 279 → 280 taken 29 times.
|
58 | config.sieve.login = sieveObj.value("login").toString(QStringLiteral("imap")); |
| 547 |
3/3✓ Branch 286 → 287 taken 29 times.
✓ Branch 287 → 288 taken 29 times.
✓ Branch 288 → 289 taken 29 times.
|
29 | config.sieve.username = sieveObj.value("username").toString(); |
| 548 |
3/3✓ Branch 293 → 294 taken 29 times.
✓ Branch 294 → 295 taken 29 times.
✓ Branch 295 → 296 taken 29 times.
|
29 | config.sieve.credentialId = sieveObj.value("credentialId").toString(); |
| 549 | } | ||
| 550 | |||
| 551 | const bool hasPlaintextPassword = | ||
| 552 |
4/4✓ Branch 302 → 303 taken 1030 times.
✓ Branch 302 → 305 taken 35 times.
✓ Branch 304 → 305 taken 4 times.
✓ Branch 304 → 306 taken 1026 times.
|
1065 | !config.imap.password.isEmpty() || !config.smtp.password.isEmpty(); |
| 553 |
2/2✓ Branch 307 → 308 taken 1042 times.
✓ Branch 307 → 353 taken 23 times.
|
1065 | if (mode == LoadMode::MetadataOnly) { |
| 554 | // Establish the stable destination identity without touching credentials. | ||
| 555 | // Preserve existing plaintext until both keyring writes are acknowledged. | ||
| 556 |
2/2✓ Branch 308 → 309 taken 9 times.
✓ Branch 308 → 335 taken 1033 times.
|
1042 | if (metadataNeedsRewrite) { |
| 557 | 9 | auto identified = root; | |
| 558 |
3/3✓ Branch 310 → 311 taken 9 times.
✓ Branch 311 → 312 taken 9 times.
✓ Branch 312 → 313 taken 9 times.
|
9 | identified.insert("id", config.id); |
| 559 |
3/3✓ Branch 315 → 316 taken 9 times.
✓ Branch 316 → 317 taken 9 times.
✓ Branch 317 → 318 taken 9 times.
|
9 | identified.insert("legacyKeyringName", config.legacyKeyringName); |
| 560 |
2/2✓ Branch 320 → 321 taken 9 times.
✓ Branch 321 → 322 taken 9 times.
|
9 | source = QJsonDocument(identified).toJson(QJsonDocument::Indented); |
| 561 |
2/3✓ Branch 325 → 326 taken 9 times.
✗ Branch 326 → 327 not taken.
✓ Branch 326 → 329 taken 9 times.
|
9 | if (!writeAccountBytes(path, source)) return std::nullopt; |
| 562 | 9 | } | |
| 563 |
6/6✓ Branch 335 → 336 taken 1008 times.
✓ Branch 335 → 338 taken 34 times.
✓ Branch 337 → 338 taken 5 times.
✓ Branch 337 → 339 taken 1003 times.
✓ Branch 340 → 341 taken 39 times.
✓ Branch 340 → 352 taken 1003 times.
|
1042 | if (hasPlaintextPassword || !config.legacyKeyringName.isEmpty()) { |
| 564 |
1/1✓ Branch 341 → 342 taken 39 times.
|
39 | config.deferredCredentialMigration = std::make_shared<AccountCredentialMigration>(); |
| 565 | 39 | config.deferredCredentialMigration->path = path; | |
| 566 |
1/1✓ Branch 347 → 348 taken 39 times.
|
39 | config.deferredCredentialMigration->digest = QCryptographicHash::hash(source, QCryptographicHash::Sha256); |
| 567 | } | ||
| 568 | 1042 | return config; | |
| 569 | } | ||
| 570 | 23 | bool imapLegacyReady = false; | |
| 571 | 23 | bool smtpLegacyReady = false; | |
| 572 |
2/2✓ Branch 354 → 355 taken 9 times.
✓ Branch 354 → 437 taken 14 times.
|
23 | if (!config.legacyKeyringName.isEmpty()) { |
| 573 | 9 | imapLegacyReady = !config.imap.password.isEmpty(); | |
| 574 | 9 | smtpLegacyReady = !config.smtp.password.isEmpty(); | |
| 575 | 9 | QByteArray password; | |
| 576 |
2/2✓ Branch 357 → 358 taken 4 times.
✓ Branch 357 → 394 taken 5 times.
|
9 | if (!imapLegacyReady) { |
| 577 | const bool stableFound = | ||
| 578 |
4/13✓ Branch 362 → 363 taken 4 times.
✗ Branch 363 → 364 not taken.
✓ Branch 363 → 367 taken 4 times.
✓ Branch 370 → 371 taken 4 times.
✗ Branch 370 → 372 not taken.
✓ Branch 372 → 373 taken 4 times.
✗ Branch 372 → 374 not taken.
✗ Branch 779 → 780 not taken.
✗ Branch 779 → 781 not taken.
✗ Branch 783 → 784 not taken.
✗ Branch 783 → 785 not taken.
✗ Branch 787 → 788 not taken.
✗ Branch 787 → 789 not taken.
|
12 | readSecretBlocking(QStringLiteral("imap"), imapCredentialName(config), &password) && |
| 579 |
1/4✗ Branch 365 → 366 not taken.
✗ Branch 365 → 367 not taken.
✓ Branch 368 → 369 taken 4 times.
✗ Branch 368 → 370 not taken.
|
4 | !password.isEmpty(); |
| 580 |
1/2✗ Branch 374 → 375 not taken.
✓ Branch 374 → 377 taken 4 times.
|
4 | if (stableFound) { |
| 581 | ✗ | config.imap.password = password; | |
| 582 | ✗ | imapLegacyReady = true; | |
| 583 | } else { | ||
| 584 |
1/1✓ Branch 377 → 378 taken 4 times.
|
4 | password.clear(); |
| 585 | const bool legacyFound = | ||
| 586 |
1/6✓ Branch 389 → 390 taken 4 times.
✗ Branch 389 → 391 not taken.
✗ Branch 791 → 792 not taken.
✗ Branch 791 → 793 not taken.
✗ Branch 795 → 796 not taken.
✗ Branch 795 → 797 not taken.
|
8 | readSecretBlocking(QStringLiteral("imap"), |
| 587 |
3/3✓ Branch 381 → 382 taken 4 times.
✓ Branch 382 → 383 taken 1 time.
✓ Branch 382 → 386 taken 3 times.
|
9 | config.legacyKeyringName, &password) && |
| 588 |
2/4✓ Branch 384 → 385 taken 1 time.
✗ Branch 384 → 386 not taken.
✓ Branch 387 → 388 taken 4 times.
✗ Branch 387 → 389 not taken.
|
5 | !password.isEmpty(); |
| 589 |
2/2✓ Branch 391 → 392 taken 1 time.
✓ Branch 391 → 394 taken 3 times.
|
4 | if (legacyFound) { |
| 590 | 1 | config.imap.password = password; | |
| 591 | 1 | imapLegacyReady = true; | |
| 592 | } | ||
| 593 | } | ||
| 594 | } | ||
| 595 | |||
| 596 |
1/1✓ Branch 394 → 395 taken 9 times.
|
9 | password.clear(); |
| 597 |
2/2✓ Branch 395 → 396 taken 4 times.
✓ Branch 395 → 432 taken 5 times.
|
9 | if (!smtpLegacyReady) { |
| 598 | const bool stableFound = | ||
| 599 |
4/13✓ Branch 400 → 401 taken 4 times.
✗ Branch 401 → 402 not taken.
✓ Branch 401 → 405 taken 4 times.
✓ Branch 408 → 409 taken 4 times.
✗ Branch 408 → 410 not taken.
✓ Branch 410 → 411 taken 4 times.
✗ Branch 410 → 412 not taken.
✗ Branch 799 → 800 not taken.
✗ Branch 799 → 801 not taken.
✗ Branch 803 → 804 not taken.
✗ Branch 803 → 805 not taken.
✗ Branch 807 → 808 not taken.
✗ Branch 807 → 809 not taken.
|
12 | readSecretBlocking(QStringLiteral("smtp"), smtpCredentialName(config), &password) && |
| 600 |
1/4✗ Branch 403 → 404 not taken.
✗ Branch 403 → 405 not taken.
✓ Branch 406 → 407 taken 4 times.
✗ Branch 406 → 408 not taken.
|
4 | !password.isEmpty(); |
| 601 |
1/2✗ Branch 412 → 413 not taken.
✓ Branch 412 → 415 taken 4 times.
|
4 | if (stableFound) { |
| 602 | ✗ | config.smtp.password = password; | |
| 603 | ✗ | smtpLegacyReady = true; | |
| 604 | } else { | ||
| 605 |
1/1✓ Branch 415 → 416 taken 4 times.
|
4 | password.clear(); |
| 606 | const bool legacyFound = | ||
| 607 |
1/6✓ Branch 427 → 428 taken 4 times.
✗ Branch 427 → 429 not taken.
✗ Branch 811 → 812 not taken.
✗ Branch 811 → 813 not taken.
✗ Branch 815 → 816 not taken.
✗ Branch 815 → 817 not taken.
|
8 | readSecretBlocking(QStringLiteral("smtp"), |
| 608 |
3/3✓ Branch 419 → 420 taken 4 times.
✓ Branch 420 → 421 taken 1 time.
✓ Branch 420 → 424 taken 3 times.
|
9 | config.legacyKeyringName, &password) && |
| 609 |
2/4✓ Branch 422 → 423 taken 1 time.
✗ Branch 422 → 424 not taken.
✓ Branch 425 → 426 taken 4 times.
✗ Branch 425 → 427 not taken.
|
5 | !password.isEmpty(); |
| 610 |
2/2✓ Branch 429 → 430 taken 1 time.
✓ Branch 429 → 432 taken 3 times.
|
4 | if (legacyFound) { |
| 611 | 1 | config.smtp.password = password; | |
| 612 | 1 | smtpLegacyReady = true; | |
| 613 | } | ||
| 614 | } | ||
| 615 | } | ||
| 616 | |||
| 617 |
3/4✓ Branch 432 → 433 taken 6 times.
✓ Branch 432 → 435 taken 3 times.
✓ Branch 433 → 434 taken 6 times.
✗ Branch 433 → 435 not taken.
|
9 | if (imapLegacyReady && smtpLegacyReady) |
| 618 | 6 | config.legacyKeyringName.clear(); | |
| 619 | 9 | metadataNeedsRewrite = true; | |
| 620 | 9 | } | |
| 621 | |||
| 622 |
3/4✓ Branch 437 → 438 taken 14 times.
✓ Branch 437 → 439 taken 9 times.
✗ Branch 438 → 439 not taken.
✓ Branch 438 → 503 taken 14 times.
|
23 | if (metadataNeedsRewrite || hasPlaintextPassword) { |
| 623 |
2/3✓ Branch 439 → 440 taken 9 times.
✓ Branch 440 → 441 taken 9 times.
✗ Branch 440 → 491 not taken.
|
9 | if (saveToFile(config, path)) { |
| 624 |
4/8✓ Branch 442 → 443 taken 6 times.
✓ Branch 442 → 445 taken 3 times.
✗ Branch 443 → 444 not taken.
✓ Branch 443 → 445 taken 6 times.
✗ Branch 444 → 445 not taken.
✗ Branch 444 → 446 not taken.
✓ Branch 447 → 448 taken 9 times.
✗ Branch 447 → 479 not taken.
|
9 | if (!config.legacyKeyringName.isEmpty() || imapLegacyReady || |
| 625 | smtpLegacyReady) { | ||
| 626 | const QString storedLegacyName = | ||
| 627 |
3/3✓ Branch 448 → 449 taken 9 times.
✓ Branch 449 → 450 taken 9 times.
✓ Branch 450 → 451 taken 9 times.
|
9 | root.value("legacyKeyringName").toString(); |
| 628 | const QString legacyName = | ||
| 629 |
1/2✓ Branch 454 → 455 taken 9 times.
✗ Branch 454 → 456 not taken.
|
9 | storedLegacyName.isEmpty() ? config.name : storedLegacyName; |
| 630 | 9 | QString deleteError; | |
| 631 |
2/2✓ Branch 458 → 459 taken 6 times.
✓ Branch 458 → 466 taken 3 times.
|
9 | if (imapLegacyReady) |
| 632 |
1/1✓ Branch 462 → 463 taken 6 times.
|
6 | deleteSecretBlocking(QStringLiteral("imap"), legacyName, |
| 633 | &deleteError); | ||
| 634 | 9 | deleteError.clear(); | |
| 635 |
2/2✓ Branch 467 → 468 taken 6 times.
✓ Branch 467 → 475 taken 3 times.
|
9 | if (smtpLegacyReady) |
| 636 |
1/1✓ Branch 471 → 472 taken 6 times.
|
6 | deleteSecretBlocking(QStringLiteral("smtp"), legacyName, |
| 637 | &deleteError); | ||
| 638 | 9 | } | |
| 639 |
5/5✓ Branch 479 → 480 taken 9 times.
✓ Branch 483 → 484 taken 9 times.
✓ Branch 484 → 485 taken 9 times.
✓ Branch 489 → 481 taken 9 times.
✓ Branch 489 → 490 taken 9 times.
|
18 | qCInfo(lcAccountConfig) << "Migrated account credential metadata:" |
| 640 |
1/1✓ Branch 485 → 486 taken 9 times.
|
9 | << path; |
| 641 | } else { | ||
| 642 | ✗ | qCWarning(lcAccountConfig) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; migration failure remains measured. | |
| 643 | << "Could not migrate account credentials; retaining" | ||
| 644 | ✗ | " the original config file:" | |
| 645 | ✗ | << path; | |
| 646 | } | ||
| 647 | } | ||
| 648 | |||
| 649 | 23 | return config; | |
| 650 | 1072 | } | |
| 651 | |||
| 652 | namespace { | ||
| 653 | // A host name goes to the socket and the TLS name check as is: no spaces, | ||
| 654 | // no control characters (Sprint 102 fuzz target account_file). | ||
| 655 | 1921 | bool hasInvalidHostCharacters(const QString &host) { | |
| 656 |
2/2✓ Branch 13 → 4 taken 18013 times.
✓ Branch 13 → 14 taken 1919 times.
|
19932 | for (const QChar character : host) |
| 657 |
5/6✓ Branch 5 → 6 taken 18011 times.
✓ Branch 5 → 8 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 18011 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 18011 times.
|
18013 | if (character.unicode() <= 0x20 || character.unicode() == 0x7f) |
| 658 | 2 | return true; | |
| 659 | 1919 | return false; | |
| 660 | } | ||
| 661 | } // namespace | ||
| 662 | |||
| 663 | 957 | QStringList AccountConfigLoader::validate(const AccountConfig &config) { | |
| 664 | 957 | QStringList errors; | |
| 665 | |||
| 666 |
2/2✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 8 taken 953 times.
|
957 | if (config.name.isEmpty()) |
| 667 |
2/2✓ Branch 4 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
|
4 | errors << "Missing 'name'"; |
| 668 |
2/2✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 14 taken 952 times.
|
957 | if (config.email.isEmpty()) |
| 669 |
2/2✓ Branch 10 → 11 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
|
5 | errors << "Missing 'email'"; |
| 670 | |||
| 671 | // IMAP validation | ||
| 672 |
2/2✓ Branch 15 → 16 taken 5 times.
✓ Branch 15 → 20 taken 952 times.
|
957 | if (config.imap.host.isEmpty()) |
| 673 |
2/2✓ Branch 16 → 17 taken 5 times.
✓ Branch 17 → 18 taken 5 times.
|
5 | errors << "Missing 'imap.host'"; |
| 674 |
2/2✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 26 taken 951 times.
|
952 | else if (hasInvalidHostCharacters(config.imap.host)) |
| 675 |
2/2✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
|
1 | errors << "Invalid characters in 'imap.host'"; |
| 676 |
2/2✓ Branch 27 → 28 taken 5 times.
✓ Branch 27 → 32 taken 952 times.
|
957 | if (config.imap.username.isEmpty()) |
| 677 |
2/2✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
|
5 | errors << "Missing 'imap.username'"; |
| 678 |
2/2✓ Branch 32 → 33 taken 4 times.
✓ Branch 32 → 37 taken 953 times.
|
957 | if (config.imap.port == 0) |
| 679 |
2/2✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
|
4 | errors << "Invalid 'imap.port'"; |
| 680 |
5/6✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 42 taken 955 times.
✓ Branch 40 → 41 taken 2 times.
✗ Branch 40 → 42 not taken.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 48 taken 955 times.
|
957 | if (config.imap.security != "ssl" && config.imap.security != "starttls") |
| 681 |
2/2✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
|
2 | errors << "Invalid 'imap.security' (must be 'ssl' or 'starttls')"; |
| 682 | |||
| 683 | // SMTP validation | ||
| 684 |
2/2✓ Branch 49 → 50 taken 5 times.
✓ Branch 49 → 54 taken 952 times.
|
957 | if (config.smtp.host.isEmpty()) |
| 685 |
2/2✓ Branch 50 → 51 taken 5 times.
✓ Branch 51 → 52 taken 5 times.
|
5 | errors << "Missing 'smtp.host'"; |
| 686 |
2/2✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 60 taken 951 times.
|
952 | else if (hasInvalidHostCharacters(config.smtp.host)) |
| 687 |
2/2✓ Branch 56 → 57 taken 1 time.
✓ Branch 57 → 58 taken 1 time.
|
1 | errors << "Invalid characters in 'smtp.host'"; |
| 688 |
2/2✓ Branch 61 → 62 taken 5 times.
✓ Branch 61 → 66 taken 952 times.
|
957 | if (config.smtp.username.isEmpty()) |
| 689 |
2/2✓ Branch 62 → 63 taken 5 times.
✓ Branch 63 → 64 taken 5 times.
|
5 | errors << "Missing 'smtp.username'"; |
| 690 |
2/2✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 71 taken 956 times.
|
957 | if (config.smtp.port == 0) |
| 691 |
2/2✓ Branch 67 → 68 taken 1 time.
✓ Branch 68 → 69 taken 1 time.
|
1 | errors << "Invalid 'smtp.port'"; |
| 692 |
6/6✓ Branch 72 → 73 taken 937 times.
✓ Branch 72 → 76 taken 20 times.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 74 → 76 taken 936 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 82 taken 956 times.
|
957 | if (config.smtp.security != "ssl" && config.smtp.security != "starttls") |
| 693 |
2/2✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
|
1 | errors << "Invalid 'smtp.security' (must be 'ssl' or 'starttls')"; |
| 694 | |||
| 695 | // Sieve validation (S95.1.3) | ||
| 696 | 957 | const SieveConfig &sieve = config.sieve; | |
| 697 |
7/8✓ Branch 83 → 84 taken 19 times.
✓ Branch 83 → 89 taken 938 times.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 85 → 89 taken 18 times.
✓ Branch 87 → 88 taken 1 time.
✗ Branch 87 → 89 not taken.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 95 taken 956 times.
|
957 | if (sieve.mode != "auto" && sieve.mode != "manual" && sieve.mode != "off") |
| 698 |
2/2✓ Branch 91 → 92 taken 1 time.
✓ Branch 92 → 93 taken 1 time.
|
1 | errors << "Invalid 'sieve.mode' (must be 'auto', 'manual' or 'off')"; |
| 699 |
2/2✓ Branch 96 → 97 taken 18 times.
✓ Branch 96 → 127 taken 939 times.
|
957 | if (sieve.mode == "manual") { |
| 700 |
3/3✓ Branch 97 → 98 taken 18 times.
✓ Branch 100 → 101 taken 1 time.
✓ Branch 100 → 105 taken 17 times.
|
18 | if (sieve.host.trimmed().isEmpty()) |
| 701 |
2/2✓ Branch 101 → 102 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
|
1 | errors << "Missing 'sieve.host'"; |
| 702 |
1/2✗ Branch 106 → 107 not taken.
✓ Branch 106 → 111 taken 17 times.
|
17 | else if (hasInvalidHostCharacters(sieve.host)) |
| 703 | ✗ | errors << "Invalid characters in 'sieve.host'"; | |
| 704 |
1/2✗ Branch 111 → 112 not taken.
✓ Branch 111 → 116 taken 18 times.
|
18 | if (sieve.port == 0) |
| 705 | ✗ | errors << "Invalid 'sieve.port'"; | |
| 706 |
5/6✓ Branch 117 → 118 taken 18 times.
✗ Branch 117 → 121 not taken.
✓ Branch 119 → 120 taken 1 time.
✓ Branch 119 → 121 taken 17 times.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 122 → 127 taken 17 times.
|
18 | if (sieve.security != "ssl" && sieve.security != "starttls") |
| 707 |
2/2✓ Branch 123 → 124 taken 1 time.
✓ Branch 124 → 125 taken 1 time.
|
1 | errors << "Invalid 'sieve.security' (must be 'ssl' or 'starttls')"; |
| 708 | } | ||
| 709 |
4/6✓ Branch 128 → 129 taken 4 times.
✓ Branch 128 → 132 taken 953 times.
✗ Branch 130 → 131 not taken.
✓ Branch 130 → 132 taken 4 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 138 taken 957 times.
|
957 | if (sieve.login != "imap" && sieve.login != "separate") |
| 710 | ✗ | errors << "Invalid 'sieve.login' (must be 'imap' or 'separate')"; | |
| 711 |
6/6✓ Branch 139 → 140 taken 4 times.
✓ Branch 139 → 143 taken 953 times.
✓ Branch 141 → 142 taken 2 times.
✓ Branch 141 → 143 taken 2 times.
✓ Branch 144 → 145 taken 2 times.
✓ Branch 144 → 149 taken 955 times.
|
957 | if (sieve.usesSeparateLogin() && sieve.username.isEmpty()) |
| 712 |
2/2✓ Branch 145 → 146 taken 2 times.
✓ Branch 146 → 147 taken 2 times.
|
2 | errors << "Missing 'sieve.username'"; |
| 713 | |||
| 714 | 957 | return errors; | |
| 715 | ✗ | } | |
| 716 | |||
| 717 | 53 | QString AccountConfigLoader::checkNameUniqueness( | |
| 718 | const AccountConfig &config, const QString &configDir) { | ||
| 719 |
1/1✓ Branch 2 → 3 taken 53 times.
|
53 | QDir dir(configDir); |
| 720 |
2/3✓ Branch 3 → 4 taken 53 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 53 times.
|
53 | if (!dir.exists()) |
| 721 | ✗ | return {}; | |
| 722 | // An unreadable file holding the chosen file name cannot be verified: | ||
| 723 | // refuse rather than overwrite it. | ||
| 724 |
2/2✓ Branch 9 → 10 taken 53 times.
✓ Branch 10 → 11 taken 53 times.
|
53 | const QString target = fileStem(config, configDir) + QStringLiteral(".json"); |
| 725 |
3/5✓ Branch 18 → 19 taken 53 times.
✓ Branch 21 → 22 taken 53 times.
✓ Branch 21 → 23 taken 53 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
|
159 | const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name); |
| 726 |
2/2✓ Branch 71 → 25 taken 49 times.
✓ Branch 71 → 72 taken 51 times.
|
100 | for (const auto &fileName : files) { |
| 727 |
2/2✓ Branch 26 → 27 taken 49 times.
✓ Branch 27 → 28 taken 49 times.
|
49 | auto existing = loadFromFile(dir.absoluteFilePath(fileName), LoadMode::MetadataOnly); |
| 728 |
2/2✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 42 taken 48 times.
|
49 | if (!existing) { |
| 729 |
1/2✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 41 not taken.
|
1 | if (fileName == target) |
| 730 |
1/1✓ Branch 37 → 38 taken 1 time.
|
2 | return QStringLiteral("Account file '%1' is already in use").arg(fileName); |
| 731 | ✗ | continue; | |
| 732 | } | ||
| 733 |
5/6✓ Branch 43 → 44 taken 48 times.
✗ Branch 43 → 48 not taken.
✓ Branch 46 → 47 taken 25 times.
✓ Branch 46 → 48 taken 23 times.
✓ Branch 49 → 50 taken 25 times.
✓ Branch 49 → 51 taken 23 times.
|
48 | if (!config.id.isEmpty() && existing->id == config.id) |
| 734 | 25 | continue; | |
| 735 |
2/2✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 62 taken 22 times.
|
23 | if (existing->name.compare(config.name, Qt::CaseInsensitive) == 0) |
| 736 | 2 | return QStringLiteral("Account name '%1' is already in use") | |
| 737 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | .arg(config.name); |
| 738 | 49 | } | |
| 739 | 51 | return {}; | |
| 740 | 106 | } | |
| 741 | |||
| 742 | 98 | QString AccountConfigLoader::fileStem(const AccountConfig &config, const QString &configDir) { | |
| 743 |
1/1✓ Branch 2 → 3 taken 98 times.
|
98 | const QString base = slugify(config.name); |
| 744 |
3/3✓ Branch 3 → 4 taken 98 times.
✓ Branch 7 → 8 taken 98 times.
✓ Branch 8 → 9 taken 98 times.
|
196 | const QString path = QDir(configDir).absoluteFilePath(base + QStringLiteral(".json")); |
| 745 |
3/3✓ Branch 13 → 14 taken 98 times.
✓ Branch 14 → 15 taken 64 times.
✓ Branch 14 → 17 taken 34 times.
|
98 | if (!QFileInfo::exists(path)) |
| 746 | 64 | return base; | |
| 747 |
1/1✓ Branch 17 → 18 taken 34 times.
|
34 | const auto owner = loadFromFile(path, LoadMode::MetadataOnly); |
| 748 | // An unreadable file may be this very account, damaged: keep its name, | ||
| 749 | // checkNameUniqueness() then refuses instead of saving a second copy. | ||
| 750 |
7/8✓ Branch 19 → 20 taken 33 times.
✓ Branch 19 → 25 taken 1 time.
✓ Branch 21 → 22 taken 33 times.
✗ Branch 21 → 26 not taken.
✓ Branch 24 → 25 taken 24 times.
✓ Branch 24 → 26 taken 9 times.
✓ Branch 27 → 28 taken 25 times.
✓ Branch 27 → 30 taken 9 times.
|
34 | if (!owner || (!config.id.isEmpty() && owner->id == config.id)) |
| 751 | 25 | return base; | |
| 752 | const QByteArray digest = | ||
| 753 |
4/4✓ Branch 30 → 31 taken 9 times.
✓ Branch 32 → 33 taken 9 times.
✓ Branch 33 → 34 taken 9 times.
✓ Branch 34 → 35 taken 9 times.
|
9 | QCryptographicHash::hash(config.name.toUtf8(), QCryptographicHash::Sha256).toHex().left(8); |
| 754 |
3/3✓ Branch 38 → 39 taken 9 times.
✓ Branch 41 → 42 taken 9 times.
✓ Branch 42 → 43 taken 9 times.
|
9 | return base + QLatin1Char('_') + QString::fromLatin1(digest); |
| 755 | 98 | } | |
| 756 | |||
| 757 | 38 | bool AccountConfigLoader::save(AccountConfig &config, const QString &configDir) { | |
| 758 |
1/1✓ Branch 2 → 3 taken 38 times.
|
38 | QDir dir(configDir); |
| 759 |
3/3✓ Branch 3 → 4 taken 38 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 21 taken 35 times.
|
38 | if (!dir.exists()) { |
| 760 |
3/4✓ Branch 5 → 6 taken 3 times.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 21 taken 3 times.
|
3 | if (!dir.mkpath(".")) { |
| 761 | ✗ | qCWarning(lcAccountConfig) | |
| 762 | ✗ | << "Failed to create config directory:" << configDir; | |
| 763 | ✗ | return false; | |
| 764 | } | ||
| 765 | } | ||
| 766 | |||
| 767 | 38 | AccountConfig storedConfig = config; | |
| 768 |
2/2✓ Branch 23 → 24 taken 26 times.
✓ Branch 23 → 29 taken 12 times.
|
38 | if (storedConfig.id.isEmpty()) |
| 769 |
2/2✓ Branch 24 → 25 taken 26 times.
✓ Branch 25 → 26 taken 26 times.
|
26 | storedConfig.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 770 | |||
| 771 | const QString uniquenessError = | ||
| 772 |
1/1✓ Branch 29 → 30 taken 38 times.
|
38 | checkNameUniqueness(storedConfig, configDir); |
| 773 |
2/2✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 44 taken 37 times.
|
38 | if (!uniquenessError.isEmpty()) { |
| 774 |
5/5✓ Branch 32 → 33 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 42 → 34 taken 1 time.
✓ Branch 42 → 43 taken 1 time.
|
2 | qCWarning(lcAccountConfig) << "Refusing to save account:" |
| 775 |
1/1✓ Branch 38 → 39 taken 1 time.
|
1 | << uniquenessError; |
| 776 | 1 | return false; | |
| 777 | } | ||
| 778 | |||
| 779 |
1/1✓ Branch 44 → 45 taken 37 times.
|
37 | auto newSlug = fileStem(storedConfig, configDir); |
| 780 |
1/1✓ Branch 45 → 46 taken 37 times.
|
37 | auto filename = newSlug + ".json"; |
| 781 |
1/1✓ Branch 46 → 47 taken 37 times.
|
37 | auto path = dir.absoluteFilePath(filename); |
| 782 | |||
| 783 | 37 | QStringList oldPaths; | |
| 784 | 37 | bool targetHasSameId = false; | |
| 785 | 12 | const auto retainLegacyCredentials = [&storedConfig](const AccountConfig &existing) { | |
| 786 | // Blank form fields mean keep. In an offline legacy profile the secrets | ||
| 787 | // may still be in the original JSON or under a display-name key. | ||
| 788 |
2/2✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 5 taken 3 times.
|
12 | if (storedConfig.imap.password.isEmpty()) storedConfig.imap.password = existing.imap.password; |
| 789 |
2/2✓ Branch 6 → 7 taken 9 times.
✓ Branch 6 → 8 taken 3 times.
|
12 | if (storedConfig.smtp.password.isEmpty()) storedConfig.smtp.password = existing.smtp.password; |
| 790 |
1/2✓ Branch 9 → 10 taken 12 times.
✗ Branch 9 → 11 not taken.
|
12 | if (storedConfig.legacyKeyringName.isEmpty()) storedConfig.legacyKeyringName = existing.legacyKeyringName; |
| 791 |
1/2✓ Branch 12 → 13 taken 12 times.
✗ Branch 12 → 14 not taken.
|
12 | if (storedConfig.imap.credentialId.isEmpty()) storedConfig.imap.credentialId = existing.imap.credentialId; |
| 792 |
1/2✓ Branch 15 → 16 taken 12 times.
✗ Branch 15 → 17 not taken.
|
12 | if (storedConfig.smtp.credentialId.isEmpty()) storedConfig.smtp.credentialId = existing.smtp.credentialId; |
| 793 |
3/6✓ Branch 18 → 19 taken 12 times.
✗ Branch 18 → 22 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 12 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 12 times.
|
12 | if (storedConfig.sieve.credentialId.isEmpty() && storedConfig.sieve.usesSeparateLogin()) |
| 794 | ✗ | storedConfig.sieve.credentialId = existing.sieve.credentialId; | |
| 795 | 49 | }; | |
| 796 |
3/3✓ Branch 47 → 48 taken 37 times.
✓ Branch 48 → 49 taken 6 times.
✓ Branch 48 → 62 taken 31 times.
|
37 | if (QFileInfo::exists(path)) { |
| 797 |
1/1✓ Branch 49 → 50 taken 6 times.
|
6 | const auto targetAccount = loadFromFile(path, LoadMode::MetadataOnly); |
| 798 |
2/4✓ Branch 51 → 52 taken 6 times.
✗ Branch 51 → 56 not taken.
✓ Branch 54 → 55 taken 6 times.
✗ Branch 54 → 56 not taken.
|
6 | targetHasSameId = targetAccount && targetAccount->id == storedConfig.id; |
| 799 |
1/2✓ Branch 57 → 58 taken 6 times.
✗ Branch 57 → 60 not taken.
|
6 | if (targetHasSameId) retainLegacyCredentials(*targetAccount); |
| 800 | 6 | } | |
| 801 |
3/5✓ Branch 66 → 67 taken 37 times.
✓ Branch 69 → 70 taken 37 times.
✓ Branch 69 → 71 taken 37 times.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 182 not taken.
|
111 | const auto files = dir.entryList({"*.json"}, QDir::Files, QDir::Name); |
| 802 |
2/2✓ Branch 99 → 73 taken 33 times.
✓ Branch 99 → 100 taken 37 times.
|
70 | for (const auto &fileName : files) { |
| 803 |
1/1✓ Branch 74 → 75 taken 33 times.
|
33 | const auto oldPath = dir.absoluteFilePath(fileName); |
| 804 |
2/2✓ Branch 76 → 77 taken 6 times.
✓ Branch 76 → 78 taken 27 times.
|
33 | if (oldPath == path) |
| 805 | 6 | continue; | |
| 806 |
1/1✓ Branch 78 → 79 taken 27 times.
|
27 | auto oldAccount = loadFromFile(oldPath, LoadMode::MetadataOnly); |
| 807 |
5/6✓ Branch 80 → 81 taken 27 times.
✗ Branch 80 → 85 not taken.
✓ Branch 83 → 84 taken 6 times.
✓ Branch 83 → 85 taken 21 times.
✓ Branch 86 → 87 taken 6 times.
✓ Branch 86 → 90 taken 21 times.
|
27 | if (oldAccount && oldAccount->id == storedConfig.id) { |
| 808 |
1/1✓ Branch 87 → 88 taken 6 times.
|
6 | oldPaths.append(oldPath); |
| 809 | 6 | retainLegacyCredentials(*oldAccount); | |
| 810 | } | ||
| 811 | 33 | } | |
| 812 | |||
| 813 |
7/8✓ Branch 101 → 102 taken 37 times.
✗ Branch 101 → 105 not taken.
✓ Branch 102 → 103 taken 6 times.
✓ Branch 102 → 106 taken 31 times.
✓ Branch 104 → 105 taken 1 time.
✓ Branch 104 → 106 taken 5 times.
✓ Branch 107 → 108 taken 1 time.
✓ Branch 107 → 125 taken 36 times.
|
37 | if (oldPaths.size() > 1 || (targetHasSameId && !oldPaths.isEmpty())) { |
| 814 | 3 | qCWarning(lcAccountConfig) // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; credential failure remains measured. | |
| 815 |
1/1✓ Branch 113 → 114 taken 1 time.
|
1 | << "Refusing ambiguous account save: duplicate account ID" |
| 816 |
3/3✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
✓ Branch 117 → 118 taken 1 time.
|
2 | << storedConfig.id << "appears in" << oldPaths; |
| 817 | 1 | return false; | |
| 818 | } | ||
| 819 | |||
| 820 |
3/3✓ Branch 125 → 126 taken 36 times.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 126 → 128 taken 35 times.
|
36 | if (!saveToFile(storedConfig, path)) |
| 821 | 1 | return false; | |
| 822 | |||
| 823 |
4/4✓ Branch 128 → 129 taken 35 times.
✓ Branch 129 → 130 taken 35 times.
✓ Branch 148 → 131 taken 4 times.
✓ Branch 148 → 149 taken 35 times.
|
39 | for (const auto &oldPath : oldPaths) { |
| 824 |
2/3✓ Branch 132 → 133 taken 4 times.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 146 taken 4 times.
|
4 | if (!QFile::remove(oldPath)) { |
| 825 | ✗ | qCWarning(lcAccountConfig) | |
| 826 | ✗ | << "Saved renamed account but could not remove old file:" << oldPath; | |
| 827 | } | ||
| 828 | } | ||
| 829 | 35 | config.id = storedConfig.id; | |
| 830 | 35 | return true; | |
| 831 | 75 | } | |
| 832 | |||
| 833 | 1 | bool AccountConfigLoader::save(const AccountConfig &config, | |
| 834 | const QString &configDir) { | ||
| 835 | 1 | AccountConfig storedConfig = config; | |
| 836 |
1/1✓ Branch 3 → 4 taken 1 time.
|
2 | return save(storedConfig, configDir); |
| 837 | 1 | } | |
| 838 | |||
| 839 | 49 | bool AccountConfigLoader::saveToFile(AccountConfig &config, | |
| 840 | const QString &path) { | ||
| 841 | 49 | AccountConfig storedConfig = config; | |
| 842 |
2/2✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 10 taken 45 times.
|
49 | if (storedConfig.id.isEmpty()) |
| 843 |
2/2✓ Branch 5 → 6 taken 4 times.
✓ Branch 6 → 7 taken 4 times.
|
4 | storedConfig.id = QUuid::createUuid().toString(QUuid::WithoutBraces); |
| 844 | |||
| 845 | 49 | QString keyringError; | |
| 846 |
1/1✓ Branch 14 → 15 taken 49 times.
|
49 | if (!writeSecretBlocking(QStringLiteral("imap"), |
| 847 |
2/2✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 31 taken 48 times.
|
98 | imapCredentialName(storedConfig), |
| 848 | storedConfig.imap.password, &keyringError)) { | ||
| 849 |
4/4✓ Branch 19 → 20 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 29 → 21 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
|
2 | qCWarning(lcAccountConfig) |
| 850 |
1/1✓ Branch 24 → 25 taken 1 time.
|
1 | << "Refusing to save account after IMAP keyring write failed:" |
| 851 |
1/1✓ Branch 25 → 26 taken 1 time.
|
1 | << keyringError; |
| 852 | 1 | return false; | |
| 853 | } | ||
| 854 |
1/1✓ Branch 35 → 36 taken 48 times.
|
48 | if (!writeSecretBlocking(QStringLiteral("smtp"), |
| 855 |
1/2✗ Branch 39 → 40 not taken.
✓ Branch 39 → 52 taken 48 times.
|
96 | smtpCredentialName(storedConfig), |
| 856 | storedConfig.smtp.password, &keyringError)) { | ||
| 857 | ✗ | qCWarning(lcAccountConfig) | |
| 858 | ✗ | << "Refusing to save account after SMTP keyring write failed:" | |
| 859 | ✗ | << keyringError; | |
| 860 | ✗ | return false; | |
| 861 | } | ||
| 862 |
2/6✗ Branch 53 → 54 not taken.
✓ Branch 53 → 57 taken 48 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 85 taken 48 times.
|
48 | if (storedConfig.sieve.usesSeparateLogin() && !storedConfig.sieve.password.isEmpty()) { |
| 863 | ✗ | if (storedConfig.sieve.credentialId.isEmpty()) | |
| 864 | ✗ | storedConfig.sieve.credentialId = QUuid::createUuid().toString(QUuid::WithoutBraces); | |
| 865 | ✗ | if (!writeSecretBlocking(QStringLiteral("sieve"), storedConfig.sieve.credentialId, | |
| 866 | storedConfig.sieve.password, &keyringError)) { | ||
| 867 | ✗ | qCWarning(lcAccountConfig) << "Refusing to save account after Sieve keyring write failed:" | |
| 868 | ✗ | << keyringError; | |
| 869 | ✗ | return false; | |
| 870 | } | ||
| 871 | } | ||
| 872 | |||
| 873 | // T-501: Use QSaveFile for atomic write — prevents TOCTOU permission race | ||
| 874 |
1/1✓ Branch 85 → 86 taken 48 times.
|
48 | QSaveFile file(path); |
| 875 |
2/3✓ Branch 87 → 88 taken 48 times.
✗ Branch 88 → 89 not taken.
✓ Branch 88 → 104 taken 48 times.
|
48 | if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { |
| 876 | ✗ | qCWarning(lcAccountConfig) | |
| 877 | ✗ | << "Cannot write config file:" << path << file.errorString(); | |
| 878 | ✗ | return false; | |
| 879 | } | ||
| 880 | |||
| 881 | // Set restrictive permissions BEFORE writing (prevents TOCTOU race) | ||
| 882 |
1/1✓ Branch 105 → 106 taken 48 times.
|
48 | file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); |
| 883 | |||
| 884 |
2/2✓ Branch 106 → 107 taken 48 times.
✓ Branch 107 → 108 taken 48 times.
|
48 | file.write(accountMetadata(storedConfig)); |
| 885 | |||
| 886 |
2/3✓ Branch 109 → 110 taken 48 times.
✗ Branch 110 → 111 not taken.
✓ Branch 110 → 123 taken 48 times.
|
48 | if (!file.commit()) { |
| 887 | ✗ | qCWarning(lcAccountConfig) | |
| 888 | ✗ | << "Failed to commit config file:" << path; | |
| 889 | ✗ | return false; | |
| 890 | } | ||
| 891 | |||
| 892 |
7/7✓ Branch 123 → 124 taken 48 times.
✓ Branch 127 → 128 taken 48 times.
✓ Branch 128 → 129 taken 48 times.
✓ Branch 129 → 130 taken 48 times.
✓ Branch 130 → 131 taken 48 times.
✓ Branch 135 → 125 taken 48 times.
✓ Branch 135 → 136 taken 48 times.
|
96 | qCInfo(lcAccountConfig) << "Saved account:" << storedConfig.id << "to" |
| 893 |
1/1✓ Branch 131 → 132 taken 48 times.
|
48 | << path; |
| 894 | 48 | config.id = storedConfig.id; | |
| 895 | 48 | return true; | |
| 896 | 49 | } | |
| 897 | |||
| 898 | 1 | bool AccountConfigLoader::saveToFile(const AccountConfig &config, | |
| 899 | const QString &path) { | ||
| 900 | 1 | AccountConfig storedConfig = config; | |
| 901 |
1/1✓ Branch 3 → 4 taken 1 time.
|
2 | return saveToFile(storedConfig, path); |
| 902 | 1 | } | |
| 903 | |||
| 904 | 1062 | bool AccountConfigLoader::isSafeId(const QString &id) { | |
| 905 | 1062 | return SafePathComponent::isSafe(id); | |
| 906 | } | ||
| 907 | |||
| 908 | // Every keyring name the account files in `dir` refer to (ids, credential | ||
| 909 | // ids and legacy names), read as plain metadata. | ||
| 910 | 10 | static QSet<QString> credentialNamesInUse(const QDir &dir) { | |
| 911 | 10 | QSet<QString> names; | |
| 912 |
3/5✓ Branch 7 → 8 taken 10 times.
✓ Branch 10 → 11 taken 10 times.
✓ Branch 10 → 12 taken 10 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
|
30 | const auto files = dir.entryList({"*.json"}, QDir::Files); |
| 913 |
2/2✓ Branch 132 → 14 taken 3 times.
✓ Branch 132 → 133 taken 10 times.
|
13 | for (const auto &fileName : files) { |
| 914 |
2/2✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
|
3 | QFile file(dir.absoluteFilePath(fileName)); |
| 915 |
2/3✓ Branch 19 → 20 taken 3 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 3 times.
|
3 | if (!file.open(QIODevice::ReadOnly)) continue; |
| 916 |
2/2✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
|
3 | const auto document = QJsonDocument::fromJson(file.readAll()); |
| 917 |
2/3✓ Branch 25 → 26 taken 3 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 3 times.
|
3 | if (!document.isObject()) continue; |
| 918 |
1/1✓ Branch 28 → 29 taken 3 times.
|
3 | const auto row = document.object(); |
| 919 | const QStringList candidates{ | ||
| 920 |
1/1✓ Branch 32 → 33 taken 3 times.
|
6 | row.value(QStringLiteral("id")).toString(), |
| 921 |
1/1✓ Branch 37 → 38 taken 3 times.
|
6 | row.value(QStringLiteral("name")).toString(), |
| 922 |
1/1✓ Branch 42 → 43 taken 3 times.
|
6 | row.value(QStringLiteral("legacyKeyringName")).toString(), |
| 923 |
3/3✓ Branch 47 → 48 taken 3 times.
✓ Branch 48 → 49 taken 3 times.
✓ Branch 52 → 53 taken 3 times.
|
9 | row.value(QStringLiteral("imap")).toObject().value(QStringLiteral("credentialId")).toString(), |
| 924 |
3/3✓ Branch 57 → 58 taken 3 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
|
9 | row.value(QStringLiteral("smtp")).toObject().value(QStringLiteral("credentialId")).toString(), |
| 925 |
5/7✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 72 → 73 taken 3 times.
✓ Branch 76 → 77 taken 18 times.
✓ Branch 76 → 78 taken 3 times.
✗ Branch 159 → 160 not taken.
✗ Branch 159 → 161 not taken.
|
27 | row.value(QStringLiteral("sieve")).toObject().value(QStringLiteral("credentialId")).toString()}; |
| 926 |
2/2✓ Branch 117 → 110 taken 18 times.
✓ Branch 117 → 118 taken 3 times.
|
21 | for (const QString &name : candidates) |
| 927 |
3/3✓ Branch 112 → 113 taken 6 times.
✓ Branch 112 → 115 taken 12 times.
✓ Branch 113 → 114 taken 6 times.
|
18 | if (!name.isEmpty()) names.insert(name); |
| 928 | 3 | } | |
| 929 | 10 | return names; | |
| 930 | 41 | } | |
| 931 | |||
| 932 | 12 | bool AccountConfigLoader::remove(const QString &accountName, | |
| 933 | const QString &configDir, | ||
| 934 | const QString &expectedId) { | ||
| 935 |
1/1✓ Branch 2 → 3 taken 12 times.
|
12 | QDir dir(configDir); |
| 936 |
2/3✓ Branch 3 → 4 taken 12 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 17 taken 12 times.
|
12 | if (!dir.exists()) { |
| 937 | ✗ | qCWarning(lcAccountConfig) | |
| 938 | ✗ | << "Config directory does not exist:" << configDir; | |
| 939 | ✗ | return false; | |
| 940 | } | ||
| 941 | |||
| 942 | // Read only identity metadata. loadFromFile() may migrate old credentials | ||
| 943 | // through a nested event loop; an account being removed needs no migration. | ||
| 944 |
3/5✓ Branch 21 → 22 taken 12 times.
✓ Branch 24 → 25 taken 12 times.
✓ Branch 24 → 26 taken 12 times.
✗ Branch 338 → 339 not taken.
✗ Branch 338 → 340 not taken.
|
36 | const auto files = dir.entryList({"*.json"}, QDir::Files); |
| 945 |
2/2✓ Branch 313 → 28 taken 12 times.
✓ Branch 313 → 314 taken 2 times.
|
14 | for (const auto &fileName : files) { |
| 946 |
1/1✓ Branch 29 → 30 taken 12 times.
|
12 | auto path = dir.absoluteFilePath(fileName); |
| 947 |
1/1✓ Branch 30 → 31 taken 12 times.
|
12 | QFile file(path); |
| 948 |
2/3✓ Branch 32 → 33 taken 12 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 12 times.
|
12 | if (!file.open(QIODevice::ReadOnly)) continue; |
| 949 |
2/2✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
|
12 | const auto document = QJsonDocument::fromJson(file.readAll()); |
| 950 |
1/1✓ Branch 38 → 39 taken 12 times.
|
12 | file.close(); |
| 951 |
2/3✓ Branch 39 → 40 taken 12 times.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 12 times.
|
12 | if (!document.isObject()) continue; |
| 952 |
1/1✓ Branch 42 → 43 taken 12 times.
|
12 | const auto row = document.object(); |
| 953 |
2/2✓ Branch 46 → 47 taken 12 times.
✓ Branch 47 → 48 taken 12 times.
|
12 | const QString id = row.value(QStringLiteral("id")).toString(); |
| 954 |
12/24✓ Branch 54 → 55 taken 12 times.
✓ Branch 55 → 56 taken 12 times.
✓ Branch 57 → 58 taken 11 times.
✓ Branch 57 → 63 taken 1 time.
✓ Branch 59 → 60 taken 6 times.
✓ Branch 59 → 62 taken 5 times.
✓ Branch 64 → 65 taken 12 times.
✗ Branch 64 → 66 not taken.
✓ Branch 66 → 67 taken 12 times.
✗ Branch 66 → 68 not taken.
✓ Branch 68 → 69 taken 12 times.
✗ Branch 68 → 70 not taken.
✓ Branch 70 → 71 taken 12 times.
✗ Branch 70 → 72 not taken.
✓ Branch 72 → 73 taken 10 times.
✓ Branch 72 → 286 taken 2 times.
✗ Branch 363 → 364 not taken.
✗ Branch 363 → 365 not taken.
✗ Branch 367 → 368 not taken.
✗ Branch 367 → 369 not taken.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 373 not taken.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 377 not taken.
|
41 | if (row.value(QStringLiteral("name")).toString() == accountName && |
| 955 |
2/2✓ Branch 61 → 62 taken 5 times.
✓ Branch 61 → 63 taken 1 time.
|
17 | (expectedId.isEmpty() || id == expectedId)) { |
| 956 |
2/3✓ Branch 73 → 74 taken 10 times.
✓ Branch 74 → 75 taken 10 times.
✗ Branch 74 → 274 not taken.
|
10 | if (QFile::remove(path)) { |
| 957 |
2/2✓ Branch 78 → 79 taken 10 times.
✓ Branch 79 → 80 taken 10 times.
|
10 | const auto imap = row.value(QStringLiteral("imap")).toObject(); |
| 958 |
2/2✓ Branch 86 → 87 taken 10 times.
✓ Branch 87 → 88 taken 10 times.
|
10 | const auto smtp = row.value(QStringLiteral("smtp")).toObject(); |
| 959 |
2/4✓ Branch 98 → 99 taken 10 times.
✓ Branch 98 → 100 taken 10 times.
✗ Branch 398 → 399 not taken.
✗ Branch 398 → 400 not taken.
|
20 | QStringList imapNames{id.isEmpty() ? accountName : id}; |
| 960 |
2/4✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 109 taken 10 times.
✗ Branch 408 → 409 not taken.
✗ Branch 408 → 410 not taken.
|
20 | QStringList smtpNames{id.isEmpty() ? accountName : id}; |
| 961 |
2/2✓ Branch 112 → 113 taken 10 times.
✓ Branch 113 → 114 taken 10 times.
|
10 | const QString imapCredential = imap.value(QStringLiteral("credentialId")).toString(); |
| 962 |
2/2✓ Branch 120 → 121 taken 10 times.
✓ Branch 121 → 122 taken 10 times.
|
10 | const QString smtpCredential = smtp.value(QStringLiteral("credentialId")).toString(); |
| 963 |
3/3✓ Branch 126 → 127 taken 2 times.
✓ Branch 126 → 128 taken 8 times.
✓ Branch 127 → 128 taken 2 times.
|
10 | if (!imapCredential.isEmpty()) imapNames.append(imapCredential); |
| 964 |
3/3✓ Branch 129 → 130 taken 2 times.
✓ Branch 129 → 131 taken 8 times.
✓ Branch 130 → 131 taken 2 times.
|
10 | if (!smtpCredential.isEmpty()) smtpNames.append(smtpCredential); |
| 965 |
2/2✓ Branch 134 → 135 taken 10 times.
✓ Branch 135 → 136 taken 10 times.
|
10 | const QString legacyName = row.value(QStringLiteral("legacyKeyringName")).toString(); |
| 966 |
1/4✗ Branch 140 → 141 not taken.
✓ Branch 140 → 143 taken 10 times.
✗ Branch 141 → 142 not taken.
✗ Branch 142 → 143 not taken.
|
10 | if (!legacyName.isEmpty()) { imapNames.append(legacyName); smtpNames.append(legacyName); } |
| 967 |
1/1✓ Branch 143 → 144 taken 10 times.
|
10 | imapNames.removeDuplicates(); |
| 968 |
1/1✓ Branch 144 → 145 taken 10 times.
|
10 | smtpNames.removeDuplicates(); |
| 969 | // S106-198: the names come from the file being deleted. A name that a | ||
| 970 | // remaining account file still uses is another account's secret and | ||
| 971 | // stays in the keyring. | ||
| 972 |
1/1✓ Branch 145 → 146 taken 10 times.
|
10 | const QSet<QString> stillUsed = credentialNamesInUse(dir); |
| 973 | 10 | QList<CredentialStore::SecretKey> keys; | |
| 974 |
4/4✓ Branch 146 → 147 taken 10 times.
✓ Branch 147 → 148 taken 10 times.
✓ Branch 162 → 149 taken 12 times.
✓ Branch 162 → 163 taken 10 times.
|
22 | for (const auto &name : imapNames) |
| 975 |
2/2✓ Branch 151 → 152 taken 11 times.
✓ Branch 151 → 160 taken 1 time.
|
23 | if (!stillUsed.contains(name)) keys.append({QStringLiteral("imap"), name}); |
| 976 |
4/4✓ Branch 163 → 164 taken 10 times.
✓ Branch 164 → 165 taken 10 times.
✓ Branch 179 → 166 taken 12 times.
✓ Branch 179 → 180 taken 10 times.
|
22 | for (const auto &name : smtpNames) |
| 977 |
1/2✓ Branch 168 → 169 taken 12 times.
✗ Branch 168 → 177 not taken.
|
24 | if (!stillUsed.contains(name)) keys.append({QStringLiteral("smtp"), name}); |
| 978 |
2/2✓ Branch 183 → 184 taken 10 times.
✓ Branch 184 → 185 taken 10 times.
|
20 | const QString sieveCredential = row.value(QStringLiteral("sieve")).toObject() |
| 979 |
2/2✓ Branch 188 → 189 taken 10 times.
✓ Branch 189 → 190 taken 10 times.
|
10 | .value(QStringLiteral("credentialId")).toString(); |
| 980 |
5/6✓ Branch 198 → 199 taken 1 time.
✓ Branch 198 → 202 taken 9 times.
✓ Branch 200 → 201 taken 1 time.
✗ Branch 200 → 202 not taken.
✓ Branch 203 → 204 taken 1 time.
✓ Branch 203 → 212 taken 9 times.
|
10 | if (!sieveCredential.isEmpty() && !stillUsed.contains(sieveCredential)) |
| 981 | 1 | keys.append({QStringLiteral("sieve"), sieveCredential}); | |
| 982 | // D-8: the local rule version history leaves with the account. The | ||
| 983 | // id is a directory name only when it is one plain path component. | ||
| 984 |
2/2✓ Branch 213 → 214 taken 8 times.
✓ Branch 213 → 231 taken 2 times.
|
10 | if (isSafeId(id)) |
| 985 |
5/5✓ Branch 214 → 215 taken 8 times.
✓ Branch 218 → 219 taken 8 times.
✓ Branch 219 → 220 taken 8 times.
✓ Branch 220 → 221 taken 8 times.
✓ Branch 221 → 222 taken 8 times.
|
24 | QDir(QDir::cleanPath(QDir(configDir).absoluteFilePath(QStringLiteral("../sieve/") + id))) |
| 986 |
1/1✓ Branch 222 → 223 taken 8 times.
|
8 | .removeRecursively(); |
| 987 |
2/2✓ Branch 232 → 233 taken 1 time.
✓ Branch 232 → 244 taken 1 time.
|
2 | else if (!id.isEmpty()) |
| 988 |
5/5✓ Branch 233 → 234 taken 1 time.
✓ Branch 237 → 238 taken 1 time.
✓ Branch 238 → 239 taken 1 time.
✓ Branch 242 → 235 taken 1 time.
✓ Branch 242 → 243 taken 1 time.
|
2 | qCWarning(lcAccountConfig) << "Removed account had an unsafe id; its rule history was left alone"; |
| 989 |
2/2✓ Branch 245 → 246 taken 10 times.
✓ Branch 247 → 248 taken 10 times.
|
10 | CredentialStore::deletePasswordsAsync(QCoreApplication::instance(), keys, |
| 990 | 20 | [accountName](bool success) { | |
| 991 |
2/2✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 15 taken 4 times.
|
8 | if (!success) |
| 992 |
4/4✓ Branch 3 → 4 taken 4 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 13 → 5 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
|
8 | qCWarning(lcAccountConfig) |
| 993 |
2/2✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
|
4 | << "Account removed, but keyring cleanup failed for" << accountName; |
| 994 | 8 | }); | |
| 995 |
4/4✓ Branch 250 → 251 taken 10 times.
✓ Branch 254 → 255 taken 10 times.
✓ Branch 262 → 252 taken 10 times.
✓ Branch 262 → 263 taken 10 times.
|
20 | qCInfo(lcAccountConfig) |
| 996 |
4/4✓ Branch 255 → 256 taken 10 times.
✓ Branch 256 → 257 taken 10 times.
✓ Branch 257 → 258 taken 10 times.
✓ Branch 258 → 259 taken 10 times.
|
10 | << "Deleted account:" << accountName << "file:" << fileName; |
| 997 | 10 | return true; | |
| 998 | 10 | } else { | |
| 999 | ✗ | qCWarning(lcAccountConfig) << "Failed to delete file:" << path; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; deletion result remains measured. | |
| 1000 | ✗ | return false; | |
| 1001 | } | ||
| 1002 | } | ||
| 1003 | 52 | } | |
| 1004 | |||
| 1005 |
4/4✓ Branch 314 → 315 taken 2 times.
✓ Branch 318 → 319 taken 2 times.
✓ Branch 324 → 316 taken 2 times.
✓ Branch 324 → 325 taken 2 times.
|
4 | qCWarning(lcAccountConfig) |
| 1006 |
2/2✓ Branch 319 → 320 taken 2 times.
✓ Branch 320 → 321 taken 2 times.
|
2 | << "No config file found for account:" << accountName; |
| 1007 | 2 | return false; | |
| 1008 | 68 | } | |
| 1009 | |||
| 1010 | 119 | QString AccountConfigLoader::slugify(const QString &name) { | |
| 1011 | // F-74 (SPRINT-104): every stripped non-ASCII code point is percent- | ||
| 1012 | // hex-encoded into the slug instead of collapsing onto one shared | ||
| 1013 | // fallback — two accounts named 北京 and 上海 (or any other purely | ||
| 1014 | // non-Latin names) previously both became "account". The slug is still | ||
| 1015 | // not injective (collapsed separators, folded accents, the 50-character | ||
| 1016 | // cut); fileStem() disambiguates the file name (S106-204). | ||
| 1017 | 119 | QString result; | |
| 1018 |
1/1✓ Branch 3 → 4 taken 119 times.
|
119 | result.reserve(name.size()); |
| 1019 | |||
| 1020 | // Normalize: lowercase, replace non-ASCII and special chars | ||
| 1021 |
2/2✓ Branch 4 → 5 taken 119 times.
✓ Branch 5 → 6 taken 119 times.
|
119 | auto normalized = name.toLower().normalized(QString::NormalizationForm_KD); |
| 1022 | |||
| 1023 |
4/4✓ Branch 7 → 8 taken 119 times.
✓ Branch 8 → 9 taken 119 times.
✓ Branch 61 → 10 taken 1354 times.
✓ Branch 61 → 62 taken 119 times.
|
1473 | for (const auto &ch : normalized) { |
| 1024 |
6/6✓ Branch 11 → 12 taken 1276 times.
✓ Branch 11 → 15 taken 78 times.
✓ Branch 13 → 14 taken 1252 times.
✓ Branch 13 → 15 taken 24 times.
✓ Branch 16 → 17 taken 1252 times.
✓ Branch 16 → 18 taken 102 times.
|
1354 | if (ch.isLetterOrNumber() && ch.unicode() < 128) { |
| 1025 |
1/1✓ Branch 17 → 60 taken 1252 times.
|
1252 | result.append(ch); |
| 1026 |
8/8✓ Branch 20 → 21 taken 56 times.
✓ Branch 20 → 27 taken 46 times.
✓ Branch 23 → 24 taken 49 times.
✓ Branch 23 → 27 taken 7 times.
✓ Branch 26 → 27 taken 5 times.
✓ Branch 26 → 28 taken 44 times.
✓ Branch 29 → 30 taken 58 times.
✓ Branch 29 → 42 taken 44 times.
|
102 | } else if (ch == ' ' || ch == '-' || ch == '_') { |
| 1027 | // Collapse consecutive separators | ||
| 1028 |
7/7✓ Branch 31 → 32 taken 53 times.
✓ Branch 31 → 37 taken 5 times.
✓ Branch 33 → 34 taken 53 times.
✓ Branch 35 → 36 taken 45 times.
✓ Branch 35 → 37 taken 8 times.
✓ Branch 38 → 39 taken 45 times.
✓ Branch 38 → 60 taken 13 times.
|
58 | if (!result.isEmpty() && result.back() != '_') { |
| 1029 |
1/1✓ Branch 40 → 41 taken 45 times.
|
45 | result.append('_'); |
| 1030 | } | ||
| 1031 |
6/6✓ Branch 43 → 44 taken 30 times.
✓ Branch 43 → 47 taken 14 times.
✓ Branch 45 → 46 taken 24 times.
✓ Branch 45 → 47 taken 6 times.
✓ Branch 48 → 49 taken 24 times.
✓ Branch 48 → 60 taken 20 times.
|
74 | } else if (ch.unicode() > 127 && |
| 1032 | 30 | ch.category() != QChar::Mark_NonSpacing) { | |
| 1033 | // Accent marks stripped by the decomposition above stay stripped | ||
| 1034 | // (Büro keeps its "buro" slug); real letters of other scripts are | ||
| 1035 | // percent-hex encoded so non-Latin names stay distinct. | ||
| 1036 |
1/1✓ Branch 50 → 51 taken 24 times.
|
24 | const QString hex = QString::number(ch.unicode(), 16); |
| 1037 |
1/1✓ Branch 52 → 53 taken 24 times.
|
24 | result.append('%'); |
| 1038 |
3/3✓ Branch 55 → 56 taken 84 times.
✓ Branch 57 → 55 taken 84 times.
✓ Branch 57 → 58 taken 24 times.
|
108 | for (const auto &nibble : hex) result.append(nibble); |
| 1039 | 24 | } | |
| 1040 | // Skip remaining ASCII special characters and combining marks | ||
| 1041 | } | ||
| 1042 | |||
| 1043 | // Remove trailing underscore | ||
| 1044 |
2/3✓ Branch 65 → 66 taken 119 times.
✗ Branch 66 → 63 not taken.
✓ Branch 66 → 67 taken 119 times.
|
119 | while (result.endsWith('_')) { |
| 1045 | ✗ | result.chop(1); | |
| 1046 | } | ||
| 1047 | |||
| 1048 | // Limit length | ||
| 1049 |
2/2✓ Branch 68 → 69 taken 7 times.
✓ Branch 68 → 75 taken 112 times.
|
119 | if (result.size() > 50) { |
| 1050 |
1/1✓ Branch 69 → 70 taken 7 times.
|
7 | result.truncate(50); |
| 1051 | // Don't end on underscore after truncation | ||
| 1052 |
3/3✓ Branch 73 → 74 taken 8 times.
✓ Branch 74 → 71 taken 1 time.
✓ Branch 74 → 75 taken 7 times.
|
8 | while (result.endsWith('_')) { |
| 1053 |
1/1✓ Branch 71 → 72 taken 1 time.
|
1 | result.chop(1); |
| 1054 | } | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | // Fallback for an empty result (an ASCII-only special-character name) | ||
| 1058 |
2/2✓ Branch 76 → 77 taken 5 times.
✓ Branch 76 → 78 taken 114 times.
|
119 | if (result.isEmpty()) { |
| 1059 |
1/1✓ Branch 77 → 78 taken 5 times.
|
5 | result = "account"; |
| 1060 | } | ||
| 1061 | |||
| 1062 | 119 | return result; | |
| 1063 | 119 | } | |
| 1064 | |||
| 1065 | // SEC-01/02: Async keyring password resolution | ||
| 1066 | 22 | bool AccountConfigLoader::finishCredentialMigration(AccountConfig &account, QObject *owner) { | |
| 1067 | 22 | const auto migration = account.deferredCredentialMigration; | |
| 1068 |
2/2✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 6 taken 9 times.
|
22 | if (!migration) return true; |
| 1069 |
6/6✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 10 taken 3 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 5 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 5 times.
|
9 | if (!migration->imapStored || !migration->smtpStored) return false; |
| 1070 |
2/8✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 5 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 20 not taken.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 5 times.
|
5 | if (!owner && !account.legacyKeyringName.isEmpty() && account.legacyKeyringName != account.id) |
| 1071 | ✗ | return false; | |
| 1072 |
1/1✓ Branch 24 → 25 taken 5 times.
|
5 | QFile source(migration->path); |
| 1073 |
3/3✓ Branch 26 → 27 taken 5 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 4 times.
|
5 | if (!source.open(QIODevice::ReadOnly)) return false; |
| 1074 |
1/1✓ Branch 29 → 30 taken 4 times.
|
4 | const auto bytes = source.readAll(); |
| 1075 |
3/3✓ Branch 32 → 33 taken 4 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 3 times.
|
4 | if (QCryptographicHash::hash(bytes, QCryptographicHash::Sha256) != migration->digest) |
| 1076 | 1 | return false; | |
| 1077 |
2/2✓ Branch 37 → 38 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
|
3 | auto root = QJsonDocument::fromJson(bytes).object(); |
| 1078 |
4/5✓ Branch 40 → 41 taken 3 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 3 times.
|
3 | if (root.value("id").toString() != account.id) return false; |
| 1079 |
2/2✓ Branch 66 → 51 taken 6 times.
✓ Branch 66 → 67 taken 3 times.
|
9 | for (const auto *protocol : {"imap", "smtp"}) { |
| 1080 |
3/3✓ Branch 51 → 52 taken 6 times.
✓ Branch 52 → 53 taken 6 times.
✓ Branch 53 → 54 taken 6 times.
|
6 | auto values = root.value(protocol).toObject(); |
| 1081 |
2/2✓ Branch 56 → 57 taken 6 times.
✓ Branch 57 → 58 taken 6 times.
|
6 | values.remove("password"); |
| 1082 |
3/3✓ Branch 59 → 60 taken 6 times.
✓ Branch 60 → 61 taken 6 times.
✓ Branch 61 → 62 taken 6 times.
|
6 | root.insert(protocol, values); |
| 1083 | 6 | } | |
| 1084 |
2/2✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
|
3 | root.remove("legacyKeyringName"); |
| 1085 |
4/5✓ Branch 70 → 71 taken 3 times.
✓ Branch 71 → 72 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 3 times.
|
3 | if (!writeAccountBytes(migration->path, QJsonDocument(root).toJson(QJsonDocument::Indented))) |
| 1086 | ✗ | return false; | |
| 1087 | 3 | const auto legacy = account.legacyKeyringName; | |
| 1088 | 3 | account.legacyKeyringName.clear(); | |
| 1089 | 3 | account.deferredCredentialMigration.reset(); | |
| 1090 |
4/8✓ Branch 81 → 82 taken 3 times.
✗ Branch 81 → 87 not taken.
✓ Branch 83 → 84 taken 3 times.
✗ Branch 83 → 87 not taken.
✓ Branch 85 → 86 taken 3 times.
✗ Branch 85 → 87 not taken.
✓ Branch 88 → 89 taken 3 times.
✗ Branch 88 → 102 not taken.
|
3 | if (owner && !legacy.isEmpty() && legacy != account.id) |
| 1091 |
3/5✓ Branch 95 → 96 taken 3 times.
✓ Branch 98 → 99 taken 6 times.
✓ Branch 98 → 100 taken 3 times.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
|
12 | CredentialStore::deletePasswordsAsync(owner, {{"imap", legacy}, {"smtp", legacy}}, {}); |
| 1092 | 3 | return true; | |
| 1093 | 25 | } | |
| 1094 | |||
| 1095 | 31 | void AccountConfigLoader::resolvePasswords( | |
| 1096 | std::vector<AccountConfig> &accounts, CredentialStore *store, | ||
| 1097 | std::function<void()> callback) { | ||
| 1098 |
3/6✓ Branch 2 → 3 taken 31 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 31 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 12 taken 31 times.
|
31 | if (!store || accounts.empty()) { |
| 1099 | ✗ | if (callback) callback(); | |
| 1100 | 1 | return; | |
| 1101 | } | ||
| 1102 | 31 | int pending = 0; | |
| 1103 |
2/2✓ Branch 33 → 14 taken 31 times.
✓ Branch 33 → 34 taken 31 times.
|
62 | for (const auto &account : accounts) { |
| 1104 |
6/6✓ Branch 16 → 17 taken 7 times.
✓ Branch 16 → 19 taken 24 times.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 20 taken 1 time.
✓ Branch 21 → 22 taken 30 times.
✓ Branch 21 → 23 taken 1 time.
|
31 | if (account.imap.password.isEmpty() || account.deferredCredentialMigration) ++pending; |
| 1105 |
6/6✓ Branch 24 → 25 taken 7 times.
✓ Branch 24 → 27 taken 24 times.
✓ Branch 26 → 27 taken 6 times.
✓ Branch 26 → 28 taken 1 time.
✓ Branch 29 → 30 taken 30 times.
✓ Branch 29 → 31 taken 1 time.
|
31 | if (account.smtp.password.isEmpty() || account.deferredCredentialMigration) ++pending; |
| 1106 | } | ||
| 1107 |
4/5✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 39 taken 30 times.
✓ Branch 36 → 37 taken 1 time.
✗ Branch 36 → 38 not taken.
✓ Branch 37 → 38 taken 1 time.
|
31 | if (!pending) { if (callback) callback(); return; } |
| 1108 |
1/1✓ Branch 39 → 40 taken 30 times.
|
30 | auto remaining = std::make_shared<int>(pending); |
| 1109 |
6/9✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 7 taken 22 times.
✓ Branch 5 → 6 taken 22 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 22 times.
✓ Branch 41 → 42 taken 30 times.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
|
74 | const auto done = [remaining, callback] { if (--*remaining == 0 && callback) callback(); }; |
| 1110 |
2/2✓ Branch 99 → 44 taken 30 times.
✓ Branch 99 → 100 taken 30 times.
|
60 | for (auto &account : accounts) { |
| 1111 |
2/2✓ Branch 96 → 47 taken 60 times.
✓ Branch 96 → 97 taken 30 times.
|
90 | for (const auto *protocol : {"imap", "smtp"}) { |
| 1112 |
1/1✓ Branch 48 → 49 taken 60 times.
|
60 | const QString service = QString::fromLatin1(protocol); |
| 1113 |
2/2✓ Branch 50 → 51 taken 30 times.
✓ Branch 50 → 52 taken 30 times.
|
60 | auto *password = service == "imap" ? &account.imap.password : &account.smtp.password; |
| 1114 | 60 | const auto migration = account.deferredCredentialMigration; | |
| 1115 |
4/6✓ Branch 55 → 56 taken 12 times.
✓ Branch 55 → 59 taken 48 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 59 taken 12 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 60 times.
|
60 | if (!password->isEmpty() && !migration) continue; |
| 1116 |
2/2✓ Branch 63 → 64 taken 30 times.
✓ Branch 63 → 65 taken 30 times.
|
60 | const QString identity = service == "imap" ? imapCredentialName(account) |
| 1117 | 60 | : smtpCredentialName(account); | |
| 1118 | 37 | const auto stored = [migration, service] { | |
| 1119 |
2/2✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 5 taken 11 times.
|
37 | if (!migration) return; |
| 1120 |
2/2✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 9 taken 5 times.
|
11 | if (service == "imap") migration->imapStored = true; |
| 1121 | 5 | else migration->smtpStored = true; | |
| 1122 | 60 | }; | |
| 1123 | 11 | const auto write = [store, identity, service, stored, done]( | |
| 1124 | const QByteArray &value, const QString &legacyCleanup = {}) { | ||
| 1125 |
1/1✓ Branch 8 → 9 taken 11 times.
|
11 | store->writePassword(service, identity, value, |
| 1126 |
2/10✓ Branch 5 → 6 taken 11 times.
✓ Branch 7 → 8 taken 11 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
|
22 | [store, identity, service, stored, done, legacyCleanup](bool success, const QString &) { |
| 1127 |
2/2✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 3 times.
|
11 | if (success) stored(); |
| 1128 |
4/8✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 10 taken 3 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 10 taken 8 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 18 taken 11 times.
|
11 | if (success && !legacyCleanup.isEmpty() && legacyCleanup != identity) |
| 1129 | ✗ | store->deletePassword(service, legacyCleanup, [done](bool) { done(); }); | |
| 1130 | 11 | else done(); | |
| 1131 | 11 | }); | |
| 1132 |
1/7✓ Branch 71 → 72 taken 60 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
|
71 | }; |
| 1133 |
1/1✓ Branch 79 → 80 taken 60 times.
|
60 | store->readPassword(service, identity, |
| 1134 |
3/13✓ Branch 76 → 77 taken 60 times.
✓ Branch 77 → 78 taken 60 times.
✓ Branch 78 → 79 taken 60 times.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
✗ Branch 134 → 135 not taken.
✗ Branch 134 → 136 not taken.
|
120 | [store, password, migration, legacy = account.legacyKeyringName, |
| 1135 | service, stored, write, done](bool success, const QByteArray &value) { | ||
| 1136 |
5/6✓ Branch 2 → 3 taken 29 times.
✓ Branch 2 → 6 taken 15 times.
✓ Branch 4 → 5 taken 29 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 29 times.
✓ Branch 7 → 12 taken 15 times.
|
44 | if (success && !value.isEmpty()) { |
| 1137 | 29 | *password = value; | |
| 1138 | 29 | stored(); | |
| 1139 | 29 | done(); | |
| 1140 | 29 | return; | |
| 1141 | } | ||
| 1142 |
3/3✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 18 taken 6 times.
✓ Branch 15 → 16 taken 9 times.
|
15 | if (!password->isEmpty()) { write(*password); return; } |
| 1143 |
2/2✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 22 taken 2 times.
|
6 | if (legacy.isEmpty()) { done(); return; } |
| 1144 |
1/1✓ Branch 27 → 28 taken 2 times.
|
2 | store->readPassword(service, legacy, |
| 1145 |
3/9✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
|
4 | [password, migration, legacy, write, done](bool found, const QByteArray &value) { |
| 1146 |
3/6✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 2 times.
|
2 | if (!found || value.isEmpty()) { done(); return; } |
| 1147 | 2 | *password = value; | |
| 1148 | // The deferred path retains fallback keys until its file commit. | ||
| 1149 |
2/3✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 2 times.
|
2 | write(value, migration ? QString{} : legacy); |
| 1150 | }); | ||
| 1151 | }); | ||
| 1152 | 60 | } | |
| 1153 | } | ||
| 1154 | 30 | } | |
| 1155 |