MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.5% 188 / 0 / 199
Functions: 100.0% 20 / 0 / 20
Branches: 85.7% 239 / 0 / 279

util/AttachmentFileSecurity.cpp
Line Branch Exec Source
1 #include "AttachmentFileSecurity.h"
2
3 #include <QFile>
4 #include <QFileInfo>
5 #include <QTemporaryFile>
6
7 #include <cerrno>
8 #include <cstdio>
9 #include <cstring>
10
11 #ifdef Q_OS_UNIX
12 #include <fcntl.h>
13 #include <unistd.h>
14 #endif
15 #ifdef Q_OS_WIN
16 #include <windows.h>
17 #endif
18
19 namespace AttachmentFileSecurity {
20
21 namespace {
22 constexpr int kMaxFileNameBytes = 255;
23
24 6 int fileNameByteLimit(const QDir &directory) {
25 #ifdef Q_OS_UNIX
26
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 3 → 4 taken 6 times.
6 const auto path = QFile::encodeName(directory.absolutePath());
27 6 const long limit = ::pathconf(path.constData(), _PC_NAME_MAX);
28
2/2
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 11 taken 1 time.
6 if (limit > 0)
29 5 return static_cast<int>(qMin<long>(kMaxFileNameBytes, limit));
30 #else
31 Q_UNUSED(directory);
32 #endif
33 1 return kMaxFileNameBytes;
34 6 }
35
36 // SEC-2026-07-27-01: Unicode bidirectional formatting characters reorder the
37 // glyphs around them without changing the bytes. An attachment named
38 // "report<U+202E>fdp.exe" therefore renders as "reportexe.pdf" in the
39 // attachment bar and in any file manager, which is the classic extension
40 // spoofing trick. These characters carry no meaning in a file name, so they
41 // are neutralized like any other control character.
42 129793 bool isBidiControl(ushort value) {
43
4/4
✓ Branch 3 → 4 taken 100587 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 4 → 5 taken 100587 times.
✓ Branch 4 → 6 taken 29205 times.
129793 return (value >= 0x202A && value <= 0x202E) || // LRE, RLE, PDF, LRO, RLO
44
2/4
✓ Branch 5 → 6 taken 100587 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 129792 times.
✗ Branch 6 → 8 not taken.
129792 (value >= 0x2066 && value <= 0x2069) || // LRI, RLI, FSI, PDI
45
3/4
✓ Branch 2 → 3 taken 100588 times.
✓ Branch 2 → 4 taken 29205 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 129792 times.
259586 value == 0x200E || // LRM
46 129793 value == 0x200F; // RLM
47 }
48
49 18 QString truncateUtf8(QString value, int maxBytes) {
50
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 18 times.
18 if (maxBytes <= 0)
51 ✗ return {};
52
1/1
✓ Branch 4 → 5 taken 18 times.
18 const QByteArray encoded = value.toUtf8();
53
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 12 times.
18 if (encoded.size() <= maxBytes)
54 6 return value;
55 12 qsizetype cut = maxBytes;
56
4/4
✓ Branch 10 → 11 taken 17 times.
✓ Branch 10 → 14 taken 3 times.
✓ Branch 15 → 9 taken 8 times.
✓ Branch 15 → 16 taken 12 times.
37 while (cut > 0 &&
57
2/2
✓ Branch 12 → 13 taken 8 times.
✓ Branch 12 → 14 taken 9 times.
17 (static_cast<unsigned char>(encoded.at(cut)) & 0xC0) == 0x80) {
58 8 --cut;
59 }
60
1/1
✓ Branch 17 → 18 taken 12 times.
12 return QString::fromUtf8(encoded.constData(), cut);
61 18 }
62
63 4139 QString cappedFileName(const QString &name, int maxBytes) {
64
3/3
✓ Branch 2 → 3 taken 4139 times.
✓ Branch 5 → 6 taken 4131 times.
✓ Branch 5 → 7 taken 8 times.
4139 if (name.toUtf8().size() <= maxBytes)
65 4131 return name;
66
67 8 const qsizetype dot = name.lastIndexOf(QLatin1Char('.'));
68
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 16 taken 7 times.
8 if (dot <= 0)
69
1/1
✓ Branch 12 → 13 taken 1 time.
1 return truncateUtf8(name, maxBytes);
70
71
1/1
✓ Branch 16 → 17 taken 7 times.
7 const QString extension = name.mid(dot);
72
1/1
✓ Branch 17 → 18 taken 7 times.
7 const int extensionBytes = extension.toUtf8().size();
73
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 26 taken 6 times.
7 if (extensionBytes >= maxBytes)
74
1/1
✓ Branch 22 → 23 taken 1 time.
1 return truncateUtf8(name, maxBytes);
75
76
2/2
✓ Branch 26 → 27 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
6 QString base = truncateUtf8(name.left(dot), maxBytes - extensionBytes);
77
2/2
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 36 taken 4 times.
6 if (base.isEmpty())
78
1/1
✓ Branch 32 → 33 taken 2 times.
2 return truncateUtf8(name, maxBytes);
79
1/1
✓ Branch 36 → 37 taken 4 times.
4 return base + extension;
80 7 }
81 } // namespace
82
83 4132 QString normalizedFileName(const QString &requestedName) {
84 // Treat attachment names as display data, never as paths. Apply the same
85 // policy on every platform so a synchronized name cannot become a path,
86 // hidden file, device name, or alternate data stream on another OS.
87 4132 QString name = requestedName;
88
1/1
✓ Branch 7 → 8 taken 4132 times.
4132 name.replace(QLatin1Char('\\'), QLatin1Char('/'));
89
1/1
✓ Branch 11 → 12 taken 4132 times.
4132 name = name.section(QLatin1Char('/'), -1);
90
1/1
✓ Branch 15 → 16 taken 4132 times.
4132 name.remove(QChar(0));
91
1/1
✓ Branch 16 → 17 taken 4132 times.
4132 name = name.trimmed();
92
93
3/4
✓ Branch 19 → 20 taken 14 times.
✓ Branch 19 → 29 taken 4118 times.
✓ Branch 21 → 22 taken 14 times.
✗ Branch 21 → 29 not taken.
4146 static const QString forbidden = QStringLiteral("<>:\"/\\|?*");
94
2/2
✓ Branch 50 → 30 taken 129796 times.
✓ Branch 50 → 51 taken 4132 times.
133928 for (qsizetype i = 0; i < name.size(); ++i) {
95 129796 const ushort value = name.at(i).unicode();
96
5/8
✓ Branch 33 → 34 taken 129793 times.
✗ Branch 33 → 41 not taken.
✓ Branch 34 → 35 taken 129793 times.
✗ Branch 34 → 41 not taken.
✓ Branch 35 → 36 taken 129793 times.
✗ Branch 35 → 41 not taken.
✓ Branch 37 → 38 taken 129792 times.
✓ Branch 37 → 41 taken 1 time.
259586 if (value < 0x20 || value == 0x7F || value == 0x2028 || value == 0x2029 ||
97
7/7
✓ Branch 32 → 33 taken 129793 times.
✓ Branch 32 → 41 taken 3 times.
✓ Branch 39 → 40 taken 129792 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 42 taken 129790 times.
✓ Branch 43 → 44 taken 6 times.
✓ Branch 43 → 48 taken 129790 times.
389382 isBidiControl(value) || forbidden.contains(name.at(i)))
98
1/1
✓ Branch 46 → 47 taken 6 times.
6 name[i] = QLatin1Char('_');
99 }
100
101
9/10
✓ Branch 54 → 55 taken 4139 times.
✗ Branch 54 → 63 not taken.
✓ Branch 57 → 58 taken 4139 times.
✓ Branch 58 → 59 taken 4134 times.
✓ Branch 58 → 62 taken 5 times.
✓ Branch 59 → 60 taken 4134 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 4132 times.
✓ Branch 64 → 52 taken 7 times.
✓ Branch 64 → 65 taken 4132 times.
4139 while (!name.isEmpty() && (name.startsWith(QLatin1Char('.')) || name.front().isSpace()))
102
1/1
✓ Branch 52 → 53 taken 7 times.
7 name.remove(0, 1);
103
2/3
✓ Branch 69 → 70 taken 4132 times.
✓ Branch 70 → 71 taken 4132 times.
✗ Branch 70 → 75 not taken.
8264 while (name.endsWith(QLatin1Char('.')) ||
104
3/5
✓ Branch 73 → 74 taken 4132 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 4132 times.
✗ Branch 77 → 66 not taken.
✓ Branch 77 → 78 taken 4132 times.
8264 name.endsWith(QLatin1Char(' ')))
105 ✗ name.chop(1);
106
107
1/2
✗ Branch 79 → 80 not taken.
✓ Branch 79 → 87 taken 4132 times.
4132 if (name.isEmpty())
108 ✗ name = QStringLiteral("attachment");
109
110 static const QSet<QString> windowsDeviceNames = {
111 14 QStringLiteral("CON"), QStringLiteral("PRN"), QStringLiteral("AUX"),
112 14 QStringLiteral("NUL"), QStringLiteral("COM1"), QStringLiteral("COM2"),
113 14 QStringLiteral("COM3"), QStringLiteral("COM4"), QStringLiteral("COM5"),
114 14 QStringLiteral("COM6"), QStringLiteral("COM7"), QStringLiteral("COM8"),
115 14 QStringLiteral("COM9"), QStringLiteral("LPT1"), QStringLiteral("LPT2"),
116 14 QStringLiteral("LPT3"), QStringLiteral("LPT4"), QStringLiteral("LPT5"),
117 14 QStringLiteral("LPT6"), QStringLiteral("LPT7"), QStringLiteral("LPT8"),
118
5/10
✓ Branch 87 → 88 taken 14 times.
✓ Branch 87 → 185 taken 4118 times.
✓ Branch 89 → 90 taken 14 times.
✗ Branch 89 → 185 not taken.
✓ Branch 160 → 161 taken 308 times.
✓ Branch 160 → 162 taken 14 times.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
✗ Branch 312 → 313 not taken.
✗ Branch 312 → 314 not taken.
4468 QStringLiteral("LPT9")};
119 8264 const auto isDeviceName = [](const QString &value) {
120
2/2
✓ Branch 5 → 6 taken 8264 times.
✓ Branch 6 → 7 taken 8264 times.
8264 return windowsDeviceNames.contains(value.section(QLatin1Char('.'), 0, 0).toUpper());
121 };
122 4133 const auto capped = [](QString value) {
123
1/1
✓ Branch 2 → 3 taken 4133 times.
4133 value = cappedFileName(value, kMaxFileNameBytes);
124 // Truncation can expose a trailing space/dot that another call removes.
125
8/10
✓ Branch 8 → 9 taken 4134 times.
✗ Branch 8 → 17 not taken.
✓ Branch 9 → 10 taken 4134 times.
✓ Branch 11 → 12 taken 4133 times.
✓ Branch 11 → 16 taken 1 time.
✓ Branch 14 → 15 taken 4133 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 4133 times.
✓ Branch 18 → 6 taken 1 time.
✓ Branch 18 → 19 taken 4133 times.
4134 while (!value.isEmpty() && (value.back().isSpace() || value.endsWith(QLatin1Char('.')))) value.chop(1);
126 4133 return value;
127 };
128
3/3
✓ Branch 185 → 186 taken 4132 times.
✓ Branch 186 → 187 taken 1 time.
✓ Branch 186 → 191 taken 4131 times.
4132 if (isDeviceName(name))
129
1/1
✓ Branch 189 → 190 taken 1 time.
1 name.prepend(QLatin1Char('_'));
130
1/1
✓ Branch 192 → 193 taken 4132 times.
4132 name = capped(name);
131 // Truncating the stem can also produce a device name ("Cont.<long
132 // extension>" → "Con.<…>"); the prefixed name starts with '_' and stays
133 // clear of the list however it is cut (Sprint 104 fuzz finding mime).
134
3/3
✓ Branch 196 → 197 taken 4132 times.
✓ Branch 197 → 198 taken 1 time.
✓ Branch 197 → 206 taken 4131 times.
4132 if (isDeviceName(name))
135
2/2
✓ Branch 200 → 201 taken 1 time.
✓ Branch 201 → 202 taken 1 time.
1 name = capped(QLatin1Char('_') + name);
136
2/4
✗ Branch 207 → 208 not taken.
✓ Branch 207 → 211 taken 4132 times.
✗ Branch 213 → 214 not taken.
✓ Branch 213 → 215 taken 4132 times.
8264 return name.isEmpty() ? QStringLiteral("attachment") : name;
137 4454 }
138
139 6 QString uniqueSavePath(const QDir &targetDir, const QString &requestedName,
140 QSet<QString> *reservedNames) {
141 // This existence check is a naming convenience only. Atomic installation
142 // and no-clobber behavior are enforced by writeAtomically() below.
143
1/1
✓ Branch 2 → 3 taken 6 times.
6 const int maxBytes = fileNameByteLimit(targetDir);
144
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 4 → 5 taken 6 times.
6 const QString fileName = cappedFileName(normalizedFileName(requestedName), maxBytes);
145
1/1
✓ Branch 6 → 7 taken 6 times.
6 const QFileInfo info(fileName);
146
1/1
✓ Branch 7 → 8 taken 6 times.
6 QString baseName = info.completeBaseName();
147
1/1
✓ Branch 8 → 9 taken 6 times.
6 QString suffix = info.suffix();
148
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 6 times.
6 if (baseName.isEmpty()) {
149 ✗ baseName = fileName;
150 ✗ suffix.clear();
151 }
152
153 6 QString candidate = fileName;
154 6 int counter = 2;
155
4/4
✓ Branch 60 → 61 taken 3 times.
✓ Branch 60 → 63 taken 7 times.
✓ Branch 62 → 63 taken 2 times.
✓ Branch 62 → 66 taken 1 time.
19 while ((reservedNames && reservedNames->contains(candidate)) ||
156
8/10
✓ Branch 63 → 64 taken 9 times.
✓ Branch 64 → 65 taken 9 times.
✓ Branch 65 → 66 taken 3 times.
✓ Branch 65 → 67 taken 6 times.
✓ Branch 68 → 69 taken 9 times.
✓ Branch 68 → 70 taken 1 time.
✓ Branch 70 → 15 taken 4 times.
✓ Branch 70 → 71 taken 6 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
19 QFileInfo::exists(targetDir.filePath(candidate))) {
157
1/1
✓ Branch 19 → 20 taken 4 times.
8 const QString marker = QStringLiteral(" (%1)").arg(counter);
158 QString extension =
159
2/3
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 4 times.
✓ Branch 27 → 28 taken 4 times.
4 suffix.isEmpty() ? QString() : QLatin1Char('.') + suffix;
160
1/1
✓ Branch 28 → 29 taken 4 times.
4 const int markerBytes = marker.toUtf8().size();
161
1/1
✓ Branch 32 → 33 taken 4 times.
8 extension = truncateUtf8(extension,
162 8 maxBytes - markerBytes - 1);
163 8 const int baseBudget = maxBytes - markerBytes -
164
1/1
✓ Branch 36 → 37 taken 4 times.
4 extension.toUtf8().size();
165
1/1
✓ Branch 40 → 41 taken 4 times.
4 QString cappedBase = truncateUtf8(baseName, baseBudget);
166
2/2
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 51 taken 3 times.
4 if (cappedBase.isEmpty())
167 1 cappedBase = QStringLiteral("a");
168
2/2
✓ Branch 51 → 52 taken 4 times.
✓ Branch 52 → 53 taken 4 times.
4 candidate = cappedBase + marker + extension;
169 4 ++counter;
170 4 }
171
2/2
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 74 taken 4 times.
6 if (reservedNames)
172
1/1
✓ Branch 72 → 73 taken 2 times.
2 reservedNames->insert(candidate);
173
1/1
✓ Branch 74 → 75 taken 6 times.
12 return targetDir.filePath(candidate);
174 6 }
175
176 7 bool writeExactly(QIODevice &device, const QByteArray &data) {
177 7 return device.write(data) == static_cast<qint64>(data.size());
178 }
179
180 namespace {
181 7 void setNativeError(QString *errorMessage, const QString &operation) {
182
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 4 times.
7 if (!errorMessage)
183 3 return;
184 #ifdef Q_OS_WIN
185 *errorMessage = QStringLiteral("%1 failed with error %2")
186 .arg(operation)
187 .arg(GetLastError());
188 #else
189 4 const int error = errno;
190 8 *errorMessage = QStringLiteral("%1: %2")
191
1/1
✓ Branch 10 → 11 taken 4 times.
12 .arg(operation,
192
1/1
✓ Branch 9 → 10 taken 4 times.
12 QString::fromLocal8Bit(std::strerror(error)));
193 #endif
194 }
195
196 // S106-224: the new directory entry reaches the disk too — after a crash
197 // right behind a successful save the fsynced data could otherwise be
198 // unreachable. Best effort: the save itself has already succeeded.
199 9 void syncParentDirectory(const QString &path) {
200 #ifdef Q_OS_UNIX
201
3/3
✓ Branch 2 → 3 taken 9 times.
✓ Branch 3 → 4 taken 9 times.
✓ Branch 4 → 5 taken 9 times.
9 const QByteArray directory = QFile::encodeName(QFileInfo(path).absolutePath());
202
1/1
✓ Branch 8 → 9 taken 9 times.
9 const int handle = ::open(directory.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
203
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 9 times.
9 if (handle < 0) return;
204
1/1
✓ Branch 11 → 12 taken 9 times.
9 ::fsync(handle);
205
1/1
✓ Branch 12 → 13 taken 9 times.
9 ::close(handle);
206 #else
207 Q_UNUSED(path);
208 #endif
209 9 }
210
211 17 bool flushToDisk(QTemporaryFile &file, QString *errorMessage) {
212
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 10 taken 17 times.
17 if (!file.flush()) {
213 ✗ if (errorMessage)
214 ✗ *errorMessage = file.errorString();
215 ✗ return false;
216 }
217 #ifdef Q_OS_UNIX
218
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 20 taken 15 times.
17 if (::fsync(file.handle()) != 0) {
219
1/1
✓ Branch 16 → 17 taken 2 times.
2 setNativeError(errorMessage, QStringLiteral("fsync"));
220 2 return false;
221 }
222 #endif
223 15 return true;
224 }
225
226 14 bool commitTemporaryFile(QTemporaryFile &file, const QString &path,
227 ExistingFilePolicy policy, QString *errorMessage) {
228
1/1
✓ Branch 2 → 3 taken 14 times.
14 const QString temporaryPath = file.fileName();
229
1/1
✓ Branch 3 → 4 taken 14 times.
14 file.close();
230
231 #ifdef Q_OS_UNIX
232
1/1
✓ Branch 4 → 5 taken 14 times.
14 const QByteArray encodedTemporary = QFile::encodeName(temporaryPath);
233
1/1
✓ Branch 5 → 6 taken 14 times.
14 const QByteArray encodedTarget = QFile::encodeName(path);
234
2/2
✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 20 taken 7 times.
14 if (policy == ExistingFilePolicy::FailIfExists) {
235 // link(2) is an atomic no-clobber installation on the same filesystem.
236
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 18 taken 4 times.
7 if (::link(encodedTemporary.constData(), encodedTarget.constData()) != 0) {
237
1/1
✓ Branch 14 → 15 taken 3 times.
3 setNativeError(errorMessage, QStringLiteral("link"));
238 3 return false;
239 }
240 // Keep autoRemove enabled so destruction removes only the temporary link.
241
1/1
✓ Branch 18 → 19 taken 4 times.
4 syncParentDirectory(path);
242 4 return true;
243 }
244
245 // rename(2) atomically replaces the directory entry. Unlike QSaveFile it
246 // does not resolve a pre-existing symlink and cannot write its target.
247
2/2
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 31 taken 5 times.
7 if (::rename(encodedTemporary.constData(), encodedTarget.constData()) != 0) {
248
1/1
✓ Branch 27 → 28 taken 2 times.
2 setNativeError(errorMessage, QStringLiteral("rename"));
249 2 return false;
250 }
251
1/1
✓ Branch 31 → 32 taken 5 times.
5 file.setAutoRemove(false);
252
1/1
✓ Branch 32 → 33 taken 5 times.
5 syncParentDirectory(path);
253 5 return true;
254 #elif defined(Q_OS_WIN)
255 const DWORD flags = MOVEFILE_WRITE_THROUGH |
256 (policy == ExistingFilePolicy::Replace
257 ? MOVEFILE_REPLACE_EXISTING
258 : DWORD(0));
259 if (!MoveFileExW(reinterpret_cast<LPCWSTR>(temporaryPath.utf16()),
260 reinterpret_cast<LPCWSTR>(path.utf16()), flags)) {
261 setNativeError(errorMessage, QStringLiteral("MoveFileExW"));
262 return false;
263 }
264 file.setAutoRemove(false);
265 return true;
266 #else
267 if (policy == ExistingFilePolicy::FailIfExists && QFileInfo::exists(path)) {
268 if (errorMessage)
269 *errorMessage = QStringLiteral("destination already exists");
270 return false;
271 }
272 if (policy == ExistingFilePolicy::Replace && QFileInfo::exists(path) &&
273 !QFile::remove(path)) {
274 if (errorMessage)
275 *errorMessage = QStringLiteral("failed to replace destination");
276 return false;
277 }
278 if (!QFile::rename(temporaryPath, path)) {
279 if (errorMessage)
280 *errorMessage = QStringLiteral("failed to install temporary file");
281 return false;
282 }
283 file.setAutoRemove(false);
284 return true;
285 #endif
286 14 }
287
288 43 bool writeAtomicallyImpl(const QString &path,
289 const std::function<bool(QIODevice &, QString *)> &producer,
290 QString *errorMessage, ExistingFilePolicy policy,
291 const std::function<void()> &beforeCommit,
292 const std::function<bool()> &cancelled = {}) {
293
2/2
✓ Branch 2 → 3 taken 33 times.
✓ Branch 2 → 4 taken 10 times.
43 if (errorMessage) errorMessage->clear();
294 58 const auto interrupted = [&] {
295
6/6
✓ Branch 3 → 4 taken 30 times.
✓ Branch 3 → 6 taken 28 times.
✓ Branch 5 → 6 taken 27 times.
✓ Branch 5 → 7 taken 3 times.
✓ Branch 8 → 9 taken 55 times.
✓ Branch 8 → 10 taken 3 times.
58 if (!cancelled || !cancelled()) return false;
296
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 18 taken 1 time.
5 if (errorMessage) *errorMessage = QStringLiteral("Attachment save cancelled");
297 3 return true;
298 43 };
299
3/3
✓ Branch 4 → 5 taken 43 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 41 times.
43 if (interrupted()) return false;
300
2/2
✓ Branch 7 → 8 taken 41 times.
✓ Branch 8 → 9 taken 41 times.
41 const QString parentPath = QFileInfo(path).absolutePath();
301
1/1
✓ Branch 10 → 11 taken 41 times.
41 const QFileInfo parentInfo(parentPath);
302
8/8
✓ Branch 11 → 12 taken 41 times.
✓ Branch 12 → 13 taken 36 times.
✓ Branch 12 → 15 taken 5 times.
✓ Branch 13 → 14 taken 36 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 34 times.
✓ Branch 17 → 18 taken 7 times.
✓ Branch 17 → 27 taken 34 times.
41 if (!parentInfo.exists() || !parentInfo.isDir()) {
303
2/2
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 26 taken 3 times.
7 if (errorMessage)
304 4 *errorMessage = QStringLiteral("destination directory does not exist");
305 7 return false;
306 }
307
308 QTemporaryFile file(
309
3/3
✓ Branch 27 → 28 taken 34 times.
✓ Branch 31 → 32 taken 34 times.
✓ Branch 32 → 33 taken 34 times.
68 QDir(parentPath).filePath(QStringLiteral(".mailjd-attachment-XXXXXX")));
310
1/1
✓ Branch 37 → 38 taken 34 times.
34 file.setAutoRemove(true);
311
3/3
✓ Branch 38 → 39 taken 34 times.
✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 46 taken 32 times.
34 if (!file.open()) {
312
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 45 taken 1 time.
2 if (errorMessage)
313
1/1
✓ Branch 41 → 42 taken 1 time.
1 *errorMessage = file.errorString();
314 2 return false;
315 }
316
2/3
✓ Branch 47 → 48 taken 32 times.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 55 taken 32 times.
32 if (!file.setPermissions(QFileDevice::ReadOwner |
317 QFileDevice::WriteOwner)) {
318 ✗ if (errorMessage)
319 ✗ *errorMessage = file.errorString();
320 ✗ return false;
321 }
322
323
7/7
✓ Branch 56 → 57 taken 30 times.
✓ Branch 56 → 59 taken 2 times.
✓ Branch 57 → 58 taken 30 times.
✓ Branch 58 → 59 taken 13 times.
✓ Branch 58 → 60 taken 17 times.
✓ Branch 61 → 62 taken 15 times.
✓ Branch 61 → 82 taken 17 times.
32 if (!producer || !producer(file, errorMessage)) {
324
6/6
✓ Branch 62 → 63 taken 13 times.
✓ Branch 62 → 66 taken 2 times.
✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 66 taken 11 times.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 81 taken 13 times.
15 if (errorMessage && errorMessage->isEmpty())
325
1/1
✓ Branch 68 → 69 taken 2 times.
2 *errorMessage = file.errorString().isEmpty()
326
3/7
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 74 taken 2 times.
✓ Branch 74 → 75 taken 2 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 79 taken 2 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
4 ? QStringLiteral("short attachment write")
327 2 : file.errorString();
328 15 return false;
329 }
330
3/3
✓ Branch 82 → 83 taken 17 times.
✓ Branch 83 → 84 taken 2 times.
✓ Branch 83 → 85 taken 15 times.
17 if (!flushToDisk(file, errorMessage))
331 2 return false;
332
333
2/2
✓ Branch 86 → 87 taken 2 times.
✓ Branch 86 → 88 taken 13 times.
15 if (beforeCommit)
334
1/1
✓ Branch 87 → 88 taken 2 times.
2 beforeCommit();
335
3/3
✓ Branch 88 → 89 taken 15 times.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 91 taken 14 times.
15 if (interrupted()) return false;
336
1/1
✓ Branch 91 → 92 taken 14 times.
14 return commitTemporaryFile(file, path, policy, errorMessage);
337 41 }
338 } // namespace
339
340 8 bool writeAtomically(const QString &path, const QByteArray &data,
341 QString *errorMessage, ExistingFilePolicy policy) {
342
1/1
✓ Branch 5 → 6 taken 8 times.
32 return writeAtomicallyImpl(path,
343 20 [&data](QIODevice &file, QString *) { return writeExactly(file, data); },
344 16 errorMessage, policy, {});
345 }
346
347 33 bool writeStreamAtomically(const QString &path,
348 const std::function<bool(QIODevice &, QString *)> &producer,
349 QString *errorMessage, ExistingFilePolicy policy,
350 const std::function<bool()> &cancelled) {
351
1/1
✓ Branch 3 → 4 taken 33 times.
33 return writeAtomicallyImpl(path, producer, errorMessage, policy, {}, cancelled);
352 }
353
354 // Compiled into the shared app-test library; declaration is visible only to
355 // unit-test translation units through MAILJD_UNIT_TEST.
356 2 bool writeAtomicallyForTest(const QString &path, const QByteArray &data,
357 const std::function<void()> &beforeCommit,
358 QString *errorMessage,
359 ExistingFilePolicy policy) {
360
1/1
✓ Branch 4 → 5 taken 2 times.
8 return writeAtomicallyImpl(path,
361 6 [&data](QIODevice &file, QString *) { return writeExactly(file, data); },
362 4 errorMessage, policy, beforeCommit);
363 }
364
365 } // namespace AttachmentFileSecurity
366