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: 100.0% 1 / 0 / 1
Functions: 100.0% 1 / 0 / 1
Branches: -% 0 / 0 / 0

ui/HtmlSanitizer.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QObject>
4 #include <QHash>
5 #include <QQueue>
6 #include <QPointer>
7 #include <QCache>
8 #include <QSet>
9 #include <QTimer>
10 #include <QThreadPool>
11 #include <atomic>
12 #include <memory>
13 #include <optional>
14 #include <QString>
15 #include <functional>
16
17 class QWebEnginePage;
18 class QWebEngineProfile;
19
20 // T-608/SEC-04: DOMPurify-based HTML sanitizer.
21 //
22 // Uses a hidden QWebEnginePage (JS enabled) to run DOMPurify.sanitize()
23 // on untrusted HTML. The sanitized output is then safe to render on the
24 // MailView page (which has JS disabled).
25 //
26 // Architecture:
27 // - On init, loads a minimal HTML skeleton that includes DOMPurify
28 // - sanitize() calls DOMPurify.sanitize(html, config) via runJavaScript()
29 // - Allowlist config: only safe tags/attributes pass through
30 // - Callback delivers clean HTML asynchronously
31 //
32 // Thread safety: must be used on the GUI thread only.
33 class HtmlSanitizer : public QObject {
34 Q_OBJECT
35 friend class TestHtmlSanitizer;
36 friend class TestMailView;
37
38 public:
39 explicit HtmlSanitizer(QObject *parent = nullptr);
40 static HtmlSanitizer *shared();
41 ~HtmlSanitizer() override;
42
43 // Initialize the sanitizer. Must be called once before sanitize().
44 // Loads DOMPurify into the hidden page. Emits ready() when done.
45 void init();
46 // Retire Chromium while its event loop is alive; reject subsequent work.
47 void shutdown();
48
49 // Returns true if the sanitizer is ready to accept sanitize() calls.
50 3 bool isReady() const { return m_state == InitializationState::Ready; }
51
52 // A resident full-HTML reader holds the DOMPurify page warm: the next
53 // HTML mail is likely, and re-creating the page after the idle retirement
54 // cost 55-85 ms per open (Sprint 90 reader gate). The page retires
55 // IdleRetentionMs after its last request without holders and
56 // HeldRetentionMs with holders. Holding a dormant sanitizer
57 // re-initializes it ahead of the next request.
58 void holdWarm(const QObject *holder, bool hold);
59 // Measurement (Sprint 90 "prepared display hit: 0 sanitizer runs"):
60 // DOMPurify executions and results served from the output cache.
61 quint64 dispatchedRequests() const { return m_dispatchedRequests; }
62 quint64 cacheHits() const { return m_cacheHits; }
63
64 // Sanitize untrusted HTML. Calls callback with clean HTML asynchronously.
65 // If the sanitizer is not ready, queues the request until ready.
66 void sanitize(const QString &dirtyHtml,
67 std::function<void(const QString &cleanHtml)> callback);
68 void sanitize(const QString &dirtyHtml,
69 const QHash<QString, QString> &inlineImages,
70 std::function<void(const QString &cleanHtml)> callback,
71 QObject *owner = nullptr, const QByteArray &cacheIdentity = {});
72 void cancel(QObject *owner);
73
74 signals:
75 void ready();
76
77 private:
78 enum class InitializationState {
79 Uninitialized,
80 Loading,
81 Ready,
82 Dormant,
83 Failed,
84 Destroying,
85 };
86
87 struct PendingRequest {
88 quint64 id = 0;
89 QString html;
90 QHash<QString, QString> inlineImages;
91 qsizetype byteSize = 0;
92 QPointer<QObject> owner;
93 bool owned = false;
94 QByteArray cacheIdentity;
95 QString cachedOutput;
96 std::shared_ptr<std::atomic_bool> cancelled = std::make_shared<std::atomic_bool>(false);
97 };
98
99 struct CachedDocument {
100 QString html;
101 QHash<QString, QString> inlineImages;
102 QString clean;
103 };
104 void dispatchRequest(PendingRequest request);
105 void completeRequest(quint64 id, const QString &cleanHtml);
106 void drainPendingRequests();
107 void failInitialization(const QString &reason);
108 // S106-041: ends the active request and replaces a page whose renderer
109 // died or whose JavaScript completion never arrived.
110 void recoverFromLostPage();
111 void failAllRequests();
112 void releasePage();
113 void scheduleIdleRelease();
114
115 QWebEnginePage *m_page = nullptr;
116 QWebEngineProfile *m_profile = nullptr;
117 QTimer m_idleRelease;
118 static constexpr int IdleRetentionMs = 5000;
119 // While a resident full-HTML reader holds the page: the next HTML mail is
120 // likely soon, but an idle Chromium renderer (~100 MiB RSS) is not kept
121 // longer than this after the last request.
122 static constexpr int HeldRetentionMs = 60000;
123 QSet<const QObject *> m_warmHolders;
124 InitializationState m_state = InitializationState::Uninitialized;
125 QQueue<PendingRequest> m_pendingRequests;
126 // Bounded asynchronous cache completions do not occupy Chromium.
127 QHash<quint64, PendingRequest> m_readyRequests;
128 QHash<quint64, std::function<void(const QString &)>> m_callbacks;
129 // Account UTF-8 payload bytes for queued and active requests together.
130 // Retained QString storage is at most twice this encoded-byte ceiling.
131 qsizetype m_pendingBytes = 0;
132 quint64 m_activeRequestId = 0;
133 int m_completionTimeoutMs = 30000; // S106-041: budget of one JavaScript sanitize
134 qsizetype m_activeBytes = 0;
135 QPointer<QObject> m_activeOwner;
136 std::optional<PendingRequest> m_activeInput;
137 QCache<QByteArray, CachedDocument> m_outputCache{2 * 1024 * 1024};
138 QThreadPool m_serializationPool;
139 quint64 m_dispatchedRequests = 0;
140 quint64 m_cacheHits = 0;
141 quint64 m_nextRequestId = 1;
142
143 static constexpr qsizetype MaxQueuedRequests = 64;
144 // Includes Base64-expanded images; keep the existing 40 MiB payload capacity.
145 static constexpr qsizetype MaxQueuedBytes = 40 * 1024 * 1024;
146 };
147