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: 90.3% 317 / 0 / 351
Functions: 90.6% 29 / 0 / 32
Branches: 69.1% 402 / 0 / 582

ui/HtmlSanitizer.cpp
Line Branch Exec Source
1 #include "HtmlSanitizer.h"
2 #include "util/Utf8ByteCount.h"
3
4 #include <QFile>
5 #include <QCoreApplication>
6 #include <utility>
7 #include <QCryptographicHash>
8 #include <QTimer>
9 #include <QJsonArray>
10 #include <QJsonDocument>
11 #include <QJsonObject>
12 #include <QLoggingCategory>
13 #include <QPointer>
14 #include <QFutureWatcher>
15 #include <QWebEnginePage>
16 #include <QWebEngineProfile>
17 #include <QWebEngineSettings>
18 #include <QWebEngineUrlRequestInterceptor>
19 #include <QWebEngineUrlRequestInfo>
20 #include <QtConcurrent>
21
22
4/7
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 52 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
54 Q_LOGGING_CATEGORY(lcSanitizer, "mailjd.sanitizer")
23
24 namespace {
25 // SEC-2026-07-21-06 / SEC-2026-07-21-07: Defense-in-depth interceptor that
26 // blocks every outgoing network request from the sanitizer profile. DOMPurify
27 // parses dirty HTML in a detached DOMParser document before stripping nodes;
28 // depending on the Chromium version that backs QtWebEngine, media/source
29 // elements in that transient parse tree could theoretically trigger resource
30 // loads. A live probe in the audit (Qt 6.8.2) observed zero requests, but this
31 // interceptor guarantees the property independently of the engine version and
32 // also limits the blast radius of a future DOMPurify/Chromium JS primitive.
33 class BlockAllRequestsInterceptor : public QWebEngineUrlRequestInterceptor {
34 public:
35 12 explicit BlockAllRequestsInterceptor(QObject *parent = nullptr)
36 12 : QWebEngineUrlRequestInterceptor(parent) {}
37
38 11 void interceptRequest(QWebEngineUrlRequestInfo &info) override {
39 // Only block external network schemes. Internal Chromium schemes (data:,
40 // blob:, about:, qrc:, chrome-extension:) are needed for the page to
41 // initialize and carry no network-exfiltration risk.
42
2/2
✓ Branch 2 → 3 taken 11 times.
✓ Branch 3 → 4 taken 11 times.
11 const QString scheme = info.requestUrl().scheme();
43
2/4
✓ Branch 10 → 11 taken 11 times.
✗ Branch 10 → 23 not taken.
✓ Branch 13 → 14 taken 11 times.
✗ Branch 13 → 23 not taken.
33 if (scheme == QLatin1String("http") || scheme == QLatin1String("https") ||
44
2/4
✓ Branch 16 → 17 taken 11 times.
✗ Branch 16 → 23 not taken.
✓ Branch 19 → 20 taken 11 times.
✗ Branch 19 → 23 not taken.
33 scheme == QLatin1String("ws") || scheme == QLatin1String("wss") ||
45
3/6
✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 23 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 11 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 11 times.
33 scheme == QLatin1String("ftp") || scheme == QLatin1String("file")) {
46 ✗ info.block(true);
47 }
48 11 }
49 };
50 } // namespace
51
52
2/2
✓ Branch 3 → 4 taken 12 times.
✓ Branch 11 → 12 taken 12 times.
12 HtmlSanitizer::HtmlSanitizer(QObject *parent) : QObject(parent) {
53
1/1
✓ Branch 12 → 13 taken 12 times.
12 m_serializationPool.setMaxThreadCount(1);
54
1/1
✓ Branch 13 → 14 taken 12 times.
12 m_serializationPool.setExpiryTimeout(-1);
55
1/1
✓ Branch 14 → 15 taken 12 times.
12 m_idleRelease.setSingleShot(true);
56
1/1
✓ Branch 15 → 16 taken 12 times.
12 connect(&m_idleRelease, &QTimer::timeout, this, [this] {
57
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 8 not taken.
3 if (m_state != InitializationState::Ready || m_activeRequestId ||
58
4/8
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 8 not taken.
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 3 times.
6 !m_pendingRequests.isEmpty() || !m_readyRequests.isEmpty()) return;
59 3 m_state = InitializationState::Dormant;
60 3 releasePage();
61 });
62
1/1
✓ Branch 18 → 19 taken 12 times.
12 connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
63 12 this, &HtmlSanitizer::shutdown);
64 12 }
65
66 19 HtmlSanitizer::~HtmlSanitizer() { shutdown(); }
67
68 ✗ HtmlSanitizer *HtmlSanitizer::shared() {
69 ✗ constexpr auto name = "mailjd-shared-html-sanitizer";
70 ✗ auto *app = QCoreApplication::instance();
71 ✗ if (auto *existing = app->findChild<HtmlSanitizer *>(QString::fromLatin1(name), Qt::FindDirectChildrenOnly))
72 ✗ return existing;
73 ✗ auto *service = new HtmlSanitizer(app);
74 ✗ service->setObjectName(QString::fromLatin1(name));
75 ✗ service->init();
76 ✗ return service;
77 }
78
79 15 void HtmlSanitizer::shutdown() {
80
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 12 times.
15 if (m_state == InitializationState::Destroying) return;
81 12 m_state = InitializationState::Destroying;
82 12 m_idleRelease.stop();
83
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 10 taken 12 times.
12 if (m_activeInput) m_activeInput->cancelled->store(true);
84 12 failAllRequests();
85
1/1
✓ Branch 12 → 13 taken 12 times.
12 m_serializationPool.waitForDone();
86 12 m_outputCache.clear();
87 12 releasePage();
88 }
89
90 507 void HtmlSanitizer::scheduleIdleRelease() {
91
6/6
✓ Branch 3 → 4 taken 404 times.
✓ Branch 3 → 7 taken 103 times.
✓ Branch 5 → 6 taken 348 times.
✓ Branch 5 → 7 taken 56 times.
✓ Branch 8 → 9 taken 348 times.
✓ Branch 8 → 14 taken 159 times.
507 if (m_readyRequests.isEmpty() && !m_idleRelease.isActive())
92
2/2
✓ Branch 10 → 11 taken 346 times.
✓ Branch 10 → 12 taken 2 times.
348 m_idleRelease.start(m_warmHolders.isEmpty() ? IdleRetentionMs : HeldRetentionMs);
93 507 }
94
95 4 void HtmlSanitizer::holdWarm(const QObject *holder, bool hold) {
96
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (!holder) return;
97
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 31 taken 2 times.
4 if (hold) {
98
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 2 times.
2 if (m_warmHolders.contains(holder)) return;
99
1/1
✓ Branch 8 → 9 taken 2 times.
2 m_warmHolders.insert(holder);
100
1/1
✓ Branch 9 → 10 taken 2 times.
2 connect(holder, &QObject::destroyed, this, [this, holder] { holdWarm(holder, false); });
101 // Re-arm with the held retention.
102
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 2 times.
2 if (m_idleRelease.isActive()) {
103 ✗ m_idleRelease.stop();
104 ✗ scheduleIdleRelease();
105 }
106
7/7
✓ Branch 15 → 16 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 27 → 17 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
4 qCDebug(lcSanitizer) << "Sanitizer held warm by" << m_warmHolders.size() << "reader(s)";
107
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 1 time.
2 if (m_state == InitializationState::Dormant) init();
108 2 return;
109 }
110
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
2 if (!m_warmHolders.remove(holder)) return;
111 2 disconnect(holder, &QObject::destroyed, this, nullptr);
112
5/10
✓ Branch 36 → 37 taken 2 times.
✗ Branch 36 → 42 not taken.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 42 not taken.
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 42 not taken.
✓ Branch 40 → 41 taken 2 times.
✗ Branch 40 → 42 not taken.
✓ Branch 43 → 44 taken 2 times.
✗ Branch 43 → 46 not taken.
4 if (m_warmHolders.isEmpty() && m_state == InitializationState::Ready && !m_activeRequestId &&
113 2 m_pendingRequests.isEmpty()) {
114 2 m_idleRelease.stop();
115 2 scheduleIdleRelease();
116 }
117 }
118
119 16 void HtmlSanitizer::releasePage() {
120 16 auto *page = std::exchange(m_page, nullptr);
121 16 auto *profile = std::exchange(m_profile, nullptr);
122
2/2
✓ Branch 4 → 5 taken 12 times.
✓ Branch 4 → 6 taken 4 times.
16 if (page) QObject::disconnect(page, nullptr, this, nullptr);
123 // JavaScript completion may run during page destruction. Its page/state
124 // guard must reject that completion before accessing Chromium again.
125
2/2
✓ Branch 6 → 7 taken 12 times.
✓ Branch 6 → 8 taken 4 times.
16 delete page;
126
2/2
✓ Branch 8 → 9 taken 12 times.
✓ Branch 8 → 11 taken 4 times.
16 if (profile) {
127 12 profile->setParent(nullptr);
128 12 profile->deleteLater();
129 }
130 16 }
131
132 13 void HtmlSanitizer::init() {
133
3/4
✓ Branch 2 → 3 taken 13 times.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 12 times.
13 if (m_page || m_state == InitializationState::Destroying)
134 1 return;
135
1/1
✓ Branch 5 → 6 taken 12 times.
12 m_idleRelease.stop();
136
137 12 m_state = InitializationState::Loading;
138
139 // Use off-the-record profile (no persistent storage)
140
3/6
✓ Branch 6 → 7 taken 12 times.
✓ Branch 7 → 8 taken 12 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12 times.
✗ Branch 125 → 126 not taken.
✗ Branch 125 → 127 not taken.
12 auto *profile = new QWebEngineProfile(this);
141 12 m_profile = profile;
142
1/1
✓ Branch 10 → 11 taken 12 times.
12 profile->setHttpCacheType(QWebEngineProfile::NoCache);
143
144 // SEC-2026-07-21-06: Install a network-blocking request interceptor as a
145 // second barrier on top of AutoLoadImages=false and the CSP meta below. We
146 // only block external network schemes (http/https/ws/wss/ftp), not internal
147 // Chromium requests (data:, blob:, qrc:, about:) — those are needed for the
148 // page itself to initialize. This still guarantees that no dirty-HTML-driven
149 // resource can escape to the network.
150
4/7
✓ Branch 11 → 12 taken 12 times.
✓ Branch 12 → 13 taken 12 times.
✓ Branch 13 → 14 taken 12 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 12 times.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 130 not taken.
12 profile->setUrlRequestInterceptor(new BlockAllRequestsInterceptor(profile));
151
152
3/6
✓ Branch 16 → 17 taken 12 times.
✓ Branch 17 → 18 taken 12 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 12 times.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 133 not taken.
12 m_page = new QWebEnginePage(profile, this);
153
154 // Enable JS on the sanitizer page (needed for DOMPurify)
155
2/2
✓ Branch 20 → 21 taken 12 times.
✓ Branch 21 → 22 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
156 // Disable everything else for security
157
2/2
✓ Branch 22 → 23 taken 12 times.
✓ Branch 23 → 24 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, false);
158
2/2
✓ Branch 24 → 25 taken 12 times.
✓ Branch 25 → 26 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false);
159
2/2
✓ Branch 26 → 27 taken 12 times.
✓ Branch 27 → 28 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
160 // SEC-2026-07-21-07: explicitly disable capabilities that are not needed for
161 // sanitizing, limiting the impact of a hypothetical JS primitive.
162
2/2
✓ Branch 28 → 29 taken 12 times.
✓ Branch 29 → 30 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false);
163
2/2
✓ Branch 30 → 31 taken 12 times.
✓ Branch 31 → 32 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
164
2/2
✓ Branch 32 → 33 taken 12 times.
✓ Branch 33 → 34 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false);
165
2/2
✓ Branch 34 → 35 taken 12 times.
✓ Branch 35 → 36 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, false);
166
2/2
✓ Branch 36 → 37 taken 12 times.
✓ Branch 37 → 38 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
167
2/2
✓ Branch 38 → 39 taken 12 times.
✓ Branch 39 → 40 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);
168
2/2
✓ Branch 40 → 41 taken 12 times.
✓ Branch 41 → 42 taken 12 times.
12 m_page->settings()->setAttribute(QWebEngineSettings::DnsPrefetchEnabled, false);
169
170 // Load DOMPurify from Qt resources
171
1/1
✓ Branch 45 → 46 taken 12 times.
12 QFile jsFile(QStringLiteral(":/dompurify/purify.min.js"));
172
2/3
✓ Branch 49 → 50 taken 12 times.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 58 taken 12 times.
12 if (!jsFile.open(QIODevice::ReadOnly)) {
173 ✗ failInitialization(QStringLiteral("Failed to load DOMPurify resource"));
174 ✗ return;
175 }
176
2/2
✓ Branch 58 → 59 taken 12 times.
✓ Branch 59 → 60 taken 12 times.
12 QString dompurifyJs = QString::fromUtf8(jsFile.readAll());
177 // The CSS filter and cid: resolver applied after DOMPurify (shared with
178 // the fuzz target sanitizer_css).
179
1/1
✓ Branch 64 → 65 taken 12 times.
12 QFile postprocessFile(QStringLiteral(":/sanitizer/postprocess.js"));
180
2/3
✓ Branch 68 → 69 taken 12 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 77 taken 12 times.
12 if (!postprocessFile.open(QIODevice::ReadOnly)) {
181 ✗ failInitialization(QStringLiteral("Failed to load the sanitizer post-processing resource"));
182 ✗ return;
183 }
184
4/4
✓ Branch 77 → 78 taken 12 times.
✓ Branch 78 → 79 taken 12 times.
✓ Branch 81 → 82 taken 12 times.
✓ Branch 82 → 83 taken 12 times.
12 dompurifyJs += QLatin1Char('\n') + QString::fromUtf8(postprocessFile.readAll());
185
186 // SEC-2026-07-21-06: strict CSP — deny all resource loads except inline
187 // script (needed for DOMPurify). default-src 'none' blocks every fetch,
188 // image, font, media and frame; script-src 'unsafe-inline' is required
189 // because DOMPurify is inlined in the page. This is defense-in-depth: the
190 // block-all interceptor and AutoLoadImages=false already prevent loads, but
191 // the CSP also constrains any engine path that bypasses the interceptor.
192 24 QString html = QStringLiteral(
193 "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
194 "<meta http-equiv=\"Content-Security-Policy\" "
195 "content=\"default-src 'none'; script-src 'unsafe-inline'; "
196 "style-src 'unsafe-inline'; img-src data:;\">"
197 "<script>%1</script></head><body></body></html>")
198
1/1
✓ Branch 90 → 91 taken 12 times.
12 .arg(dompurifyJs);
199
200 // Load the page and wait for it to finish
201
1/1
✓ Branch 93 → 94 taken 12 times.
12 const QPointer<HtmlSanitizer> guard(this);
202
1/1
✓ Branch 94 → 95 taken 12 times.
12 const QPointer<QWebEnginePage> pageGuard(m_page);
203 // S106-041: a renderer that dies takes the pending JavaScript completion
204 // with it. Without recovery the active request never completed and every
205 // later sanitize queued behind it until restart.
206
1/1
✓ Branch 96 → 97 taken 12 times.
12 connect(m_page, &QWebEnginePage::renderProcessTerminated, this,
207 24 [this, pageGuard](QWebEnginePage::RenderProcessTerminationStatus status, int code) {
208 ✗ if (!pageGuard || m_page != pageGuard) return;
209 ✗ qCWarning(lcSanitizer) << "Sanitizer renderer ended" << int(status) << code;
210 ✗ recoverFromLostPage();
211 });
212 12 connect(m_page, &QWebEnginePage::loadFinished, this,
213
1/3
✓ Branch 101 → 102 taken 12 times.
✗ Branch 187 → 188 not taken.
✗ Branch 187 → 189 not taken.
24 [this, guard, pageGuard](bool ok) {
214
4/8
✓ Branch 3 → 4 taken 11 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 11 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 9 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 11 times.
22 if (!guard || !pageGuard || m_page != pageGuard ||
215
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 11 times.
11 m_state != InitializationState::Loading) return;
216
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 21 taken 11 times.
11 if (!ok) {
217 ✗ failInitialization(QStringLiteral("DOMPurify page load failed"));
218 ✗ return;
219 }
220
221 // Verify DOMPurify is available
222
1/1
✓ Branch 27 → 28 taken 11 times.
11 m_page->runJavaScript(
223 22 QStringLiteral("typeof DOMPurify !== 'undefined'"),
224
1/3
✓ Branch 23 → 24 taken 11 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
22 [this, guard, pageGuard](const QVariant &result) {
225
4/8
✓ Branch 3 → 4 taken 11 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 11 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 9 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 11 times.
22 if (!guard || !pageGuard || m_page != pageGuard ||
226
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 11 times.
11 m_state != InitializationState::Loading) return;
227
1/2
✓ Branch 14 → 15 taken 11 times.
✗ Branch 14 → 29 not taken.
11 if (result.toBool()) {
228
5/5
✓ Branch 15 → 16 taken 11 times.
✓ Branch 19 → 20 taken 11 times.
✓ Branch 20 → 21 taken 11 times.
✓ Branch 24 → 17 taken 11 times.
✓ Branch 24 → 25 taken 11 times.
22 qCInfo(lcSanitizer) << "DOMPurify sanitizer ready";
229 11 m_state = InitializationState::Ready;
230 11 emit ready();
231
1/2
✓ Branch 27 → 28 taken 11 times.
✗ Branch 27 → 36 not taken.
11 if (guard) drainPendingRequests();
232 } else {
233 ✗ failInitialization(
234 ✗ QStringLiteral("DOMPurify not found after page load"));
235 }
236 });
237 });
238
239
2/2
✓ Branch 104 → 105 taken 12 times.
✓ Branch 105 → 106 taken 12 times.
12 m_page->setHtml(html);
240 12 }
241
242 396 void HtmlSanitizer::sanitize(
243 const QString &dirtyHtml,
244 std::function<void(const QString &cleanHtml)> callback) {
245
1/1
✓ Branch 6 → 7 taken 396 times.
396 sanitize(dirtyHtml, {}, std::move(callback));
246 396 }
247
248 724 void HtmlSanitizer::sanitize(
249 const QString &dirtyHtml, const QHash<QString, QString> &inlineImages,
250 std::function<void(const QString &cleanHtml)> callback, QObject *owner,
251 const QByteArray &cacheIdentity) {
252
253
3/3
✓ Branch 2 → 3 taken 163 times.
✓ Branch 2 → 4 taken 561 times.
✓ Branch 3 → 4 taken 163 times.
724 if (owner) cancel(owner);
254
255
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 22 taken 724 times.
724 if (m_state == InitializationState::Uninitialized) {
256 ✗ qCWarning(lcSanitizer) << "Sanitizer not initialized, call init() first";
257 ✗ if (callback)
258 ✗ callback(QString()); // Return empty to be safe
259 151 return;
260 }
261
262
1/2
✓ Branch 22 → 23 taken 724 times.
✗ Branch 22 → 24 not taken.
724 if (m_state == InitializationState::Failed ||
263
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 31 taken 723 times.
724 m_state == InitializationState::Destroying) {
264
1/2
✓ Branch 25 → 26 taken 1 time.
✗ Branch 25 → 30 not taken.
1 if (callback)
265
1/1
✓ Branch 27 → 28 taken 1 time.
1 callback(QString());
266 1 return;
267 }
268
269
1/1
✓ Branch 32 → 33 taken 723 times.
723 qsizetype byteSize = MailJD::utf8ByteCountUpTo(dirtyHtml, MaxQueuedBytes);
270 723 for (auto it = inlineImages.cbegin();
271
5/6
✓ Branch 43 → 44 taken 733 times.
✗ Branch 43 → 48 not taken.
✓ Branch 46 → 47 taken 10 times.
✓ Branch 46 → 48 taken 723 times.
✓ Branch 49 → 34 taken 10 times.
✓ Branch 49 → 50 taken 723 times.
733 byteSize <= MaxQueuedBytes && it != inlineImages.cend(); ++it) {
272
1/1
✓ Branch 36 → 37 taken 10 times.
10 const qsizetype keyBytes = MailJD::utf8ByteCountUpTo(
273 it.key(), MaxQueuedBytes - byteSize);
274
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 10 times.
10 if (keyBytes > MaxQueuedBytes - byteSize) { byteSize = MaxQueuedBytes + 1; break; }
275 10 byteSize += keyBytes;
276
1/1
✓ Branch 41 → 42 taken 10 times.
10 const qsizetype valueBytes = MailJD::utf8ByteCountUpTo(
277 it.value(), MaxQueuedBytes - byteSize);
278 10 byteSize += valueBytes;
279 }
280
10/16
✓ Branch 51 → 52 taken 519 times.
✓ Branch 51 → 53 taken 204 times.
✓ Branch 54 → 55 taken 204 times.
✓ Branch 57 → 58 taken 204 times.
✓ Branch 58 → 59 taken 204 times.
✓ Branch 58 → 60 taken 519 times.
✓ Branch 60 → 61 taken 204 times.
✓ Branch 60 → 62 taken 519 times.
✓ Branch 62 → 63 taken 204 times.
✓ Branch 62 → 64 taken 519 times.
✗ Branch 178 → 179 not taken.
✗ Branch 178 → 180 not taken.
✗ Branch 182 → 183 not taken.
✗ Branch 182 → 184 not taken.
✗ Branch 186 → 187 not taken.
✗ Branch 186 → 188 not taken.
723 const QByteArray identity = cacheIdentity.isEmpty() ? QByteArray{} : QByteArrayLiteral("html-policy-1:") + QCryptographicHash::hash(cacheIdentity, QCryptographicHash::Sha256);
281 723 QString cachedOutput;
282
8/8
✓ Branch 64 → 65 taken 81 times.
✓ Branch 64 → 66 taken 642 times.
✓ Branch 65 → 66 taken 4 times.
✓ Branch 65 → 69 taken 77 times.
✓ Branch 67 → 68 taken 201 times.
✓ Branch 67 → 69 taken 445 times.
✓ Branch 70 → 71 taken 201 times.
✓ Branch 70 → 85 taken 522 times.
723 if ((m_state == InitializationState::Ready || m_state == InitializationState::Dormant) && !identity.isEmpty()) {
283 201 if (const auto *entry = m_outputCache.object(identity);
284
8/8
✓ Branch 72 → 73 taken 149 times.
✓ Branch 72 → 78 taken 52 times.
✓ Branch 74 → 75 taken 148 times.
✓ Branch 74 → 78 taken 1 time.
✓ Branch 76 → 77 taken 147 times.
✓ Branch 76 → 78 taken 1 time.
✓ Branch 79 → 80 taken 147 times.
✓ Branch 79 → 85 taken 54 times.
201 entry && entry->html == dirtyHtml && entry->inlineImages == inlineImages) {
285 147 cachedOutput = entry->clean;
286
1/2
✓ Branch 81 → 82 taken 147 times.
✗ Branch 81 → 85 not taken.
147 if (byteSize <= MaxQueuedBytes)
287
1/1
✓ Branch 83 → 84 taken 147 times.
147 byteSize += MailJD::utf8ByteCountUpTo(cachedOutput,
288 MaxQueuedBytes - byteSize);
289 }
290 }
291
6/6
✓ Branch 87 → 88 taken 166 times.
✓ Branch 87 → 89 taken 557 times.
✓ Branch 90 → 91 taken 715 times.
✓ Branch 90 → 92 taken 8 times.
✓ Branch 94 → 95 taken 38 times.
✓ Branch 94 → 112 taken 685 times.
1438 if (m_pendingRequests.size() + m_readyRequests.size() + (m_activeRequestId != 0 ? 1 : 0) >= MaxQueuedRequests ||
292
2/2
✓ Branch 91 → 92 taken 30 times.
✓ Branch 91 → 93 taken 685 times.
715 byteSize > MaxQueuedBytes - m_pendingBytes) {
293
5/5
✓ Branch 95 → 96 taken 38 times.
✓ Branch 99 → 100 taken 38 times.
✓ Branch 100 → 101 taken 38 times.
✓ Branch 104 → 97 taken 38 times.
✓ Branch 104 → 105 taken 38 times.
76 qCWarning(lcSanitizer) << "Sanitizer retained-work limit exceeded";
294
1/2
✓ Branch 106 → 107 taken 38 times.
✗ Branch 106 → 111 not taken.
38 if (callback)
295
1/1
✓ Branch 108 → 109 taken 38 times.
38 callback(QString());
296 38 return;
297 }
298
299
1/1
✓ Branch 112 → 113 taken 685 times.
685 m_idleRelease.stop();
300 685 const quint64 requestId = m_nextRequestId++;
301
1/2
✓ Branch 114 → 115 taken 685 times.
✗ Branch 114 → 128 not taken.
685 if (callback) {
302
2/2
✓ Branch 115 → 116 taken 163 times.
✓ Branch 115 → 126 taken 522 times.
685 if (owner) {
303
1/1
✓ Branch 116 → 117 taken 163 times.
163 const QPointer<QObject> guard(owner);
304
2/4
✓ Branch 120 → 121 taken 163 times.
✓ Branch 121 → 122 taken 163 times.
✗ Branch 204 → 205 not taken.
✗ Branch 204 → 206 not taken.
163 m_callbacks.insert(requestId, [guard, callback = std::move(callback)](const QString &clean) {
305
2/2
✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 5 taken 2 times.
62 if (guard) callback(clean);
306 62 });
307
1/1
✓ Branch 127 → 128 taken 522 times.
685 } else m_callbacks.insert(requestId, std::move(callback));
308 }
309
310
2/12
✓ Branch 130 → 131 taken 685 times.
✓ Branch 133 → 134 taken 685 times.
✗ Branch 212 → 213 not taken.
✗ Branch 212 → 214 not taken.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 221 → 222 not taken.
✗ Branch 221 → 223 not taken.
✗ Branch 224 → 225 not taken.
✗ Branch 224 → 226 not taken.
685 PendingRequest request{requestId, dirtyHtml, inlineImages, byteSize, owner, owner != nullptr, identity, cachedOutput};
311 685 m_pendingBytes += byteSize;
312
2/2
✓ Branch 135 → 136 taken 112 times.
✓ Branch 135 → 140 taken 573 times.
685 if (!cachedOutput.isNull()) {
313
1/1
✓ Branch 137 → 138 taken 112 times.
112 m_readyRequests.insert(requestId, std::move(request));
314
1/1
✓ Branch 138 → 139 taken 112 times.
112 QTimer::singleShot(0, this, [this, requestId] {
315 112 const auto ready = m_readyRequests.constFind(requestId);
316
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 110 times.
112 if (ready == m_readyRequests.cend()) return;
317 110 const QString clean = ready->cachedOutput;
318 110 ++m_cacheHits;
319
1/1
✓ Branch 9 → 10 taken 110 times.
110 completeRequest(requestId, clean);
320 110 });
321 112 return;
322 }
323
3/3
✓ Branch 140 → 141 taken 156 times.
✓ Branch 140 → 143 taken 417 times.
✓ Branch 142 → 145 taken 156 times.
573 if (owner) m_pendingRequests.prepend(std::move(request));
324
1/1
✓ Branch 144 → 145 taken 417 times.
417 else m_pendingRequests.enqueue(std::move(request));
325
3/3
✓ Branch 145 → 146 taken 3 times.
✓ Branch 145 → 147 taken 570 times.
✓ Branch 146 → 147 taken 3 times.
573 if (m_state == InitializationState::Dormant) init();
326
1/1
✓ Branch 147 → 148 taken 573 times.
573 drainPendingRequests();
327 985 }
328
329 164 void HtmlSanitizer::cancel(QObject *owner) {
330
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 164 times.
164 if (!owner) return;
331
2/2
✓ Branch 17 → 5 taken 99 times.
✓ Branch 17 → 18 taken 164 times.
263 for (qsizetype i = m_pendingRequests.size(); i > 0; --i) {
332 99 const auto &request = m_pendingRequests[i - 1];
333
6/8
✓ Branch 6 → 7 taken 99 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 99 times.
✗ Branch 8 → 11 not taken.
✓ Branch 10 → 11 taken 98 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 98 times.
✓ Branch 13 → 16 taken 1 time.
99 if (request.owned && (!request.owner || request.owner == owner)) {
334 98 m_pendingBytes -= request.byteSize;
335 98 m_callbacks.remove(request.id);
336 98 m_pendingRequests.removeAt(i - 1);
337 }
338 }
339
3/3
✓ Branch 18 → 19 taken 164 times.
✓ Branch 40 → 20 taken 1 time.
✓ Branch 40 → 41 taken 164 times.
165 for (auto it = m_readyRequests.begin(); it != m_readyRequests.end();) {
340
4/8
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 29 not taken.
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 28 not taken.
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 29 not taken.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 37 not taken.
1 if (it->owned && (!it->owner || it->owner == owner)) {
341 1 m_pendingBytes -= it->byteSize;
342
1/1
✓ Branch 33 → 34 taken 1 time.
1 m_callbacks.remove(it->id);
343
1/1
✓ Branch 35 → 36 taken 1 time.
1 it = m_readyRequests.erase(it);
344 ✗ } else ++it;
345 }
346
2/2
✓ Branch 42 → 43 taken 99 times.
✓ Branch 42 → 49 taken 65 times.
164 if (m_activeOwner == owner) {
347
1/2
✓ Branch 44 → 45 taken 99 times.
✗ Branch 44 → 48 not taken.
99 if (m_activeInput) m_activeInput->cancelled->store(true);
348 99 m_callbacks.remove(m_activeRequestId);
349 }
350
5/6
✓ Branch 49 → 50 taken 62 times.
✓ Branch 49 → 53 taken 102 times.
✓ Branch 51 → 52 taken 62 times.
✗ Branch 51 → 53 not taken.
✓ Branch 54 → 55 taken 62 times.
✓ Branch 54 → 56 taken 102 times.
164 if (!m_activeRequestId && m_pendingRequests.isEmpty()) drainPendingRequests();
351 }
352
353 405 void HtmlSanitizer::dispatchRequest(PendingRequest request) {
354 405 m_activeRequestId = request.id;
355 405 m_activeBytes = request.byteSize;
356 405 m_activeOwner = request.owner;
357
2/4
✓ Branch 3 → 4 taken 405 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 9 taken 405 times.
405 if (m_state != InitializationState::Ready || !m_page) {
358 ✗ completeRequest(request.id, QString());
359 ✗ return;
360 }
361
362 // A preceding active request may have populated the cache while this
363 // consumer waited. Recheck before dispatch to share that immutable result.
364
5/6
✓ Branch 10 → 11 taken 405 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 56 times.
✓ Branch 12 → 14 taken 349 times.
✓ Branch 15 → 16 taken 56 times.
✓ Branch 15 → 30 taken 349 times.
405 if (request.cachedOutput.isNull() && !request.cacheIdentity.isEmpty()) {
365 56 if (const auto *entry = m_outputCache.object(request.cacheIdentity);
366
8/8
✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 23 taken 53 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 23 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 30 taken 55 times.
56 entry && entry->html == request.html && entry->inlineImages == request.inlineImages) {
367
1/1
✓ Branch 26 → 27 taken 1 time.
1 const qsizetype extra = MailJD::utf8ByteCountUpTo(
368 1 entry->clean, MaxQueuedBytes - m_pendingBytes);
369
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 30 not taken.
1 if (extra <= MaxQueuedBytes - m_pendingBytes) {
370 1 request.cachedOutput = entry->clean;
371 1 request.byteSize += extra;
372 1 m_activeBytes += extra;
373 1 m_pendingBytes += extra;
374 }
375 }
376 }
377
378 405 m_activeInput = request;
379
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 39 taken 404 times.
405 if (!request.cachedOutput.isNull()) {
380 1 ++m_cacheHits;
381 1 const quint64 id = request.id;
382 1 const QString clean = request.cachedOutput;
383
1/1
✓ Branch 35 → 36 taken 1 time.
2 QTimer::singleShot(0, this, [this, id, clean] { completeRequest(id, clean); });
384 1 return;
385 1 }
386
387 404 const quint64 requestId = request.id;
388
2/5
✓ Branch 40 → 41 taken 404 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 404 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
404 auto *watcher = new QFutureWatcher<QString>(this);
389 404 connect(watcher, &QFutureWatcher<QString>::finished, this,
390
1/1
✓ Branch 43 → 44 taken 404 times.
404 [this, watcher, requestId] {
391
1/1
✓ Branch 2 → 3 taken 404 times.
404 const QString js = watcher->result();
392
1/1
✓ Branch 3 → 4 taken 404 times.
404 watcher->deleteLater();
393
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 404 times.
404 if (requestId != m_activeRequestId) return;
394
3/4
✓ Branch 7 → 8 taken 404 times.
✗ Branch 7 → 21 not taken.
✓ Branch 9 → 10 taken 403 times.
✓ Branch 9 → 21 taken 1 time.
404 if (m_state != InitializationState::Ready || !m_page || js.isEmpty() ||
395
5/8
✓ Branch 6 → 7 taken 404 times.
✗ Branch 6 → 21 not taken.
✓ Branch 11 → 12 taken 403 times.
✗ Branch 11 → 21 not taken.
✓ Branch 15 → 16 taken 403 times.
✗ Branch 15 → 21 not taken.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 28 taken 402 times.
1211 !m_activeInput || m_activeInput->cancelled->load() ||
396
4/4
✓ Branch 17 → 18 taken 55 times.
✓ Branch 17 → 22 taken 348 times.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 54 times.
403 (m_activeInput->owned && !m_activeInput->owner)) {
397
1/1
✓ Branch 25 → 26 taken 2 times.
2 completeRequest(requestId, QString());
398 2 return;
399 }
400
1/1
✓ Branch 28 → 29 taken 402 times.
402 const QPointer<HtmlSanitizer> guard(this);
401 402 ++m_dispatchedRequests;
402
2/4
✓ Branch 30 → 31 taken 402 times.
✓ Branch 31 → 32 taken 402 times.
✗ Branch 50 → 51 not taken.
✗ Branch 50 → 52 not taken.
402 m_page->runJavaScript(js, [guard, requestId](const QVariant &result) {
403
3/4
✓ Branch 3 → 4 taken 402 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 402 times.
✓ Branch 6 → 7 taken 402 times.
402 if (guard) guard->completeRequest(requestId, result.toString());
404 402 });
405 // S106-041: a completion that never arrives (hung or lost renderer)
406 // ends the request after a generous budget and replaces the page.
407
1/1
✓ Branch 34 → 35 taken 402 times.
402 QTimer::singleShot(m_completionTimeoutMs, this, [this, requestId] {
408
2/2
✓ Branch 2 → 3 taken 269 times.
✓ Branch 2 → 4 taken 1 time.
270 if (requestId != m_activeRequestId) return;
409
5/5
✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
2 qCWarning(lcSanitizer) << "Sanitizer completion lost; replacing the page";
410 1 recoverFromLostPage();
411 });
412
2/2
✓ Branch 38 → 39 taken 402 times.
✓ Branch 38 → 41 taken 2 times.
404 });
413
1/1
✓ Branch 49 → 50 taken 404 times.
404 watcher->setFuture(QtConcurrent::run(&m_serializationPool,
414
1/5
✓ Branch 48 → 49 taken 404 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
1212 [html = request.html, images = request.inlineImages,
415 404 cancelled = request.cancelled]() -> QString {
416
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 404 times.
404 if (cancelled->load()) return {};
417 // Serialize untrusted input as JSON. This avoids executable template-string
418 // interpolation for backticks, ${...}, quotes, and Unicode separators.
419
1/1
✓ Branch 6 → 7 taken 404 times.
404 QJsonObject inlineImages;
420
2/2
✓ Branch 16 → 8 taken 9 times.
✓ Branch 16 → 17 taken 404 times.
413 for (auto it = images.cbegin(); it != images.cend(); ++it)
421
2/2
✓ Branch 9 → 10 taken 9 times.
✓ Branch 11 → 12 taken 9 times.
9 inlineImages.insert(it.key(), it.value());
422 const QByteArray serialized =
423
3/5
✓ Branch 20 → 21 taken 404 times.
✓ Branch 25 → 26 taken 808 times.
✓ Branch 25 → 27 taken 404 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
1616 QJsonDocument(QJsonArray{html, inlineImages})
424
1/1
✓ Branch 21 → 22 taken 404 times.
404 .toJson(QJsonDocument::Compact);
425
5/6
✓ Branch 29 → 30 taken 403 times.
✓ Branch 29 → 32 taken 1 time.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 403 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 403 times.
404 if (cancelled->load() || serialized.isEmpty()) return {};
426
427 // DOMPurify config:
428 // - FORBID_TAGS: dangerous tags that should never appear in email
429 // - FORBID_ATTR: event handlers. The inline style attribute stays allowed
430 // but is rewritten through the CSS declaration allowlist below — a
431 // blanket ban (418caf9) destroyed the layout of every real-world HTML
432 // mail, which is almost entirely table + inline-style based.
433 // - ALLOW_DATA_ATTR: false — no data-* attributes
434 // - ADD_ATTR: allow 'target' for links (mailto: etc.)
435 806 QString js = QStringLiteral(
436 "(function() {"
437 " try {"
438 " var payload = %1;"
439 " var dirty = payload[0];"
440 " var cidImages = payload[1];"
441 " var clean = DOMPurify.sanitize(dirty, {"
442 " FORBID_TAGS: ['style', 'math', 'svg', 'form', 'input',"
443 " 'textarea', 'select', 'button', 'object',"
444 " 'embed', 'applet', 'base', 'meta', 'link'],"
445 " FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover',"
446 " 'onfocus', 'onblur', 'onsubmit'],"
447 " ALLOW_DATA_ATTR: false,"
448 " ADD_ATTR: ['target']"
449 " });"
450 " var template = document.createElement('template');"
451 " template.innerHTML = clean;"
452 // resources/sanitizer/postprocess.js: CSS declaration allowlist and
453 // cid: resolution. The template document fragment is inert, so
454 // nothing here can trigger a load while the filter runs.
455 " template.content.querySelectorAll('[style]').forEach(function(el) {"
456 " var kept = mailjdFilterStyle(el.getAttribute('style'));"
457 " if (kept.length) el.setAttribute('style', kept);"
458 " else el.removeAttribute('style');"
459 " });"
460 " template.content.querySelectorAll('img[src]').forEach(function(img) {"
461 " var resolved = mailjdResolveCid(img.getAttribute('src'), cidImages);"
462 " if (resolved !== null) img.setAttribute('src', resolved);"
463 " });"
464 " return template.innerHTML;"
465 " } catch(e) {"
466 " return '';"
467 " }"
468 "})()")
469
2/2
✓ Branch 40 → 41 taken 403 times.
✓ Branch 41 → 42 taken 403 times.
403 .arg(QString::fromUtf8(serialized));
470
471
1/2
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 403 times.
403 return cancelled->load() ? QString{} : js;
472
3/7
✓ Branch 17 → 18 taken 404 times.
✓ Branch 18 → 19 taken 404 times.
✓ Branch 19 → 20 taken 404 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 72 not taken.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
808 }));
473 }
474
475 586 void HtmlSanitizer::completeRequest(quint64 id, const QString &cleanHtml) {
476
3/3
✓ Branch 2 → 3 taken 586 times.
✓ Branch 5 → 6 taken 111 times.
✓ Branch 5 → 10 taken 475 times.
586 if (auto ready = m_readyRequests.find(id); ready != m_readyRequests.end()) {
477 111 m_pendingBytes -= ready->byteSize;
478
1/1
✓ Branch 8 → 9 taken 111 times.
111 m_readyRequests.erase(ready);
479 }
480
2/2
✓ Branch 10 → 11 taken 405 times.
✓ Branch 10 → 55 taken 181 times.
586 if (id == m_activeRequestId) {
481
2/2
✓ Branch 16 → 17 taken 403 times.
✓ Branch 16 → 27 taken 2 times.
810 if (m_activeInput && !m_activeInput->cancelled->load() &&
482
4/4
✓ Branch 19 → 20 taken 56 times.
✓ Branch 19 → 27 taken 347 times.
✓ Branch 22 → 23 taken 55 times.
✓ Branch 22 → 27 taken 1 time.
459 !m_activeInput->cacheIdentity.isEmpty() &&
483
5/8
✓ Branch 12 → 13 taken 405 times.
✗ Branch 12 → 27 not taken.
✓ Branch 24 → 25 taken 55 times.
✗ Branch 24 → 27 not taken.
✓ Branch 25 → 26 taken 55 times.
✗ Branch 25 → 27 not taken.
✓ Branch 28 → 29 taken 55 times.
✓ Branch 28 → 53 taken 350 times.
866 m_activeInput->cachedOutput.isNull() && !cleanHtml.isEmpty() && m_state == InitializationState::Ready) {
484 55 const auto &input = *m_activeInput;
485 55 qsizetype bytes = 512 + input.cacheIdentity.size() + 2 * (input.html.size() + cleanHtml.size());
486
2/2
✓ Branch 41 → 34 taken 4 times.
✓ Branch 41 → 42 taken 55 times.
59 for (auto it = input.inlineImages.cbegin(); it != input.inlineImages.cend(); ++it)
487 4 bytes += 128 + 2 * (it.key().size() + it.value().size());
488
1/2
✓ Branch 43 → 44 taken 55 times.
✗ Branch 43 → 53 not taken.
55 if (bytes <= m_outputCache.maxCost())
489
1/4
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 52 taken 55 times.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 77 not taken.
55 m_outputCache.insert(input.cacheIdentity,
490
2/6
✓ Branch 44 → 45 taken 55 times.
✓ Branch 48 → 49 taken 55 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
55 new CachedDocument{input.html, input.inlineImages, cleanHtml}, int(bytes));
491 }
492 405 m_activeInput.reset();
493 405 m_pendingBytes -= m_activeBytes;
494 405 m_activeBytes = 0;
495 405 m_activeRequestId = 0;
496
1/1
✓ Branch 54 → 55 taken 405 times.
405 m_activeOwner = nullptr;
497 }
498
1/1
✓ Branch 55 → 56 taken 586 times.
586 auto callback = m_callbacks.take(id);
499
1/1
✓ Branch 56 → 57 taken 586 times.
586 const QPointer<HtmlSanitizer> guard(this);
500
3/3
✓ Branch 58 → 59 taken 584 times.
✓ Branch 58 → 60 taken 2 times.
✓ Branch 59 → 60 taken 584 times.
586 if (callback) callback(cleanHtml);
501
2/3
✓ Branch 61 → 62 taken 586 times.
✗ Branch 61 → 63 not taken.
✓ Branch 62 → 63 taken 586 times.
586 if (guard) drainPendingRequests();
502 586 }
503
504 1233 void HtmlSanitizer::drainPendingRequests() {
505
4/4
✓ Branch 2 → 3 taken 1077 times.
✓ Branch 2 → 4 taken 156 times.
✓ Branch 3 → 4 taken 166 times.
✓ Branch 3 → 5 taken 911 times.
1233 if (m_state != InitializationState::Ready || m_activeRequestId) return;
506
2/2
✓ Branch 6 → 7 taken 505 times.
✓ Branch 6 → 9 taken 406 times.
911 if (m_pendingRequests.isEmpty()) {
507 505 scheduleIdleRelease();
508 505 return;
509 }
510
2/2
✓ Branch 29 → 10 taken 406 times.
✓ Branch 29 → 30 taken 1 time.
407 while (!m_pendingRequests.isEmpty()) {
511
1/1
✓ Branch 10 → 11 taken 406 times.
406 auto request = m_pendingRequests.dequeue();
512
6/6
✓ Branch 11 → 12 taken 58 times.
✓ Branch 11 → 15 taken 348 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 57 times.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 405 times.
406 if (request.owned && !request.owner) {
513 1 m_pendingBytes -= request.byteSize;
514
1/1
✓ Branch 17 → 18 taken 1 time.
1 m_callbacks.remove(request.id);
515 1 continue;
516 }
517
1/1
✓ Branch 21 → 22 taken 405 times.
405 dispatchRequest(std::move(request));
518 405 return;
519 406 }
520 1 drainPendingRequests();
521 }
522
523 1 void HtmlSanitizer::recoverFromLostPage() {
524 // The active request ends empty (shown as "could not be sanitized"); the
525 // page is released and the next request starts a fresh one (Dormant).
526
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 13 not taken.
1 if (m_activeRequestId) {
527
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
1 if (m_activeInput) m_activeInput->cancelled->store(true);
528 1 const quint64 lost = m_activeRequestId;
529 1 m_state = InitializationState::Dormant;
530 1 releasePage();
531
1/1
✓ Branch 10 → 11 taken 1 time.
1 completeRequest(lost, QString());
532 } else {
533 ✗ m_state = InitializationState::Dormant;
534 ✗ releasePage();
535 }
536
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
1 if (!m_pendingRequests.isEmpty()) init();
537 1 }
538
539 2 void HtmlSanitizer::failInitialization(const QString &reason) {
540
5/5
✓ Branch 2 → 3 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 11 → 4 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
4 qCCritical(lcSanitizer) << reason;
541 2 m_state = InitializationState::Failed;
542 2 m_outputCache.clear();
543 2 failAllRequests();
544 2 }
545
546 14 void HtmlSanitizer::failAllRequests() {
547
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 14 times.
14 if (m_activeInput) m_activeInput->cancelled->store(true);
548 // Complete queued requests in submission order. Move any already-dispatched
549 // callbacks out afterwards so page destruction also has exactly-once
550 // semantics even though QWebEngine may suppress JavaScript completions.
551
2/2
✓ Branch 15 → 8 taken 69 times.
✓ Branch 15 → 16 taken 14 times.
83 while (!m_pendingRequests.isEmpty()) {
552
1/1
✓ Branch 8 → 9 taken 69 times.
69 const PendingRequest request = m_pendingRequests.dequeue();
553 69 m_pendingBytes -= request.byteSize;
554
1/1
✓ Branch 10 → 11 taken 69 times.
69 completeRequest(request.id, QString());
555 69 }
556
2/2
✓ Branch 24 → 17 taken 1 time.
✓ Branch 24 → 25 taken 14 times.
15 while (!m_readyRequests.isEmpty())
557
1/1
✓ Branch 20 → 21 taken 1 time.
1 completeRequest(m_readyRequests.cbegin().key(), QString());
558
1/3
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 30 taken 14 times.
✗ Branch 27 → 28 not taken.
14 if (m_activeRequestId) completeRequest(m_activeRequestId, QString());
559 14 m_pendingBytes = 0;
560
561 14 auto inFlightCallbacks = std::move(m_callbacks);
562 14 m_callbacks.clear();
563
2/4
✓ Branch 33 → 34 taken 14 times.
✗ Branch 36 → 37 not taken.
✗ Branch 46 → 35 not taken.
✓ Branch 46 → 47 taken 14 times.
14 for (auto callback : inFlightCallbacks) {
564 ✗ if (callback)
565 ✗ callback(QString());
566 ✗ }
567 14 }
568