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: 96.5% 222 / 0 / 230
Functions: 100.0% 13 / 0 / 13
Branches: 87.0% 327 / 0 / 376

service/DavPushXml.cpp
Line Branch Exec Source
1 #include "DavPushXml.h"
2
3 #include "WebPushPayloadCodec.h"
4
5 #include <QDateTime>
6 #include <QRegularExpression>
7 #include <QTimeZone>
8 #include <QXmlStreamReader>
9 #include <QXmlStreamWriter>
10 #include <climits>
11
12 namespace DavPushXml {
13 namespace {
14
15 const QString kDav = QStringLiteral("DAV:");
16
17 10 void setError(QString *error, const QString &text) {
18
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 2 times.
10 if (error) *error = text;
19 10 }
20
21 822 bool isPush(const QXmlStreamReader &reader, QStringView name) {
22
6/6
✓ Branch 3 → 4 taken 822 times.
✓ Branch 5 → 6 taken 816 times.
✓ Branch 5 → 10 taken 6 times.
✓ Branch 6 → 7 taken 816 times.
✓ Branch 8 → 9 taken 508 times.
✓ Branch 8 → 10 taken 308 times.
822 return reader.namespaceUri() == kNamespace && reader.name() == name;
23 }
24
25 2623 bool isDav(const QXmlStreamReader &reader, QStringView name) {
26
6/6
✓ Branch 3 → 4 taken 2623 times.
✓ Branch 5 → 6 taken 2622 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 6 → 7 taken 2622 times.
✓ Branch 8 → 9 taken 1929 times.
✓ Branch 8 → 10 taken 693 times.
2623 return reader.namespaceUri() == kDav && reader.name() == name;
27 }
28
29 // The push properties found in one successful propstat of one response.
30 struct RawOffer {
31 bool topicSeen = false;
32 QString topic;
33 bool transportsSeen = false;
34 bool webPush = false;
35 bool vapidSeen = false;
36 QString vapidText;
37 QString vapidType;
38 bool triggersSeen = false;
39 int contentUpdateDepth = -1; // -1 = no content-update trigger
40 };
41
42 1 int parseDepth(const QString &text) {
43
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
1 const QString value = text.trimmed().toLower();
44
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (value == QLatin1String("infinity")) return INT_MAX;
45 1 bool ok = false;
46
1/1
✓ Branch 9 → 10 taken 1 time.
1 const int depth = value.toInt(&ok);
47
2/4
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
1 return ok && depth >= 0 ? depth : -1;
48 1 }
49
50 // Reads the children of P:transports / P:supported-triggers into `offer`.
51
1/1
✓ Branch 2 → 3 taken 367 times.
367 void readPushProperty(QXmlStreamReader &reader, RawOffer *offer) {
52
3/3
✓ Branch 11 → 12 taken 367 times.
✓ Branch 12 → 13 taken 149 times.
✓ Branch 12 → 19 taken 218 times.
367 if (isPush(reader, u"topic")) {
53 149 offer->topicSeen = true;
54
2/2
✓ Branch 13 → 14 taken 149 times.
✓ Branch 14 → 15 taken 149 times.
149 offer->topic = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
55 149 return;
56 }
57
3/3
✓ Branch 28 → 29 taken 218 times.
✓ Branch 29 → 30 taken 149 times.
✓ Branch 29 → 79 taken 69 times.
218 if (isPush(reader, u"transports")) {
58 149 offer->transportsSeen = true;
59
2/2
✓ Branch 77 → 31 taken 92 times.
✓ Branch 77 → 78 taken 149 times.
241 while (reader.readNextStartElement()) {
60
3/3
✓ Branch 40 → 41 taken 92 times.
✓ Branch 41 → 42 taken 90 times.
✓ Branch 41 → 75 taken 2 times.
92 if (isPush(reader, u"web-push")) {
61 90 offer->webPush = true;
62
2/2
✓ Branch 74 → 43 taken 13 times.
✓ Branch 74 → 76 taken 90 times.
103 while (reader.readNextStartElement()) {
63
2/3
✓ Branch 52 → 53 taken 13 times.
✓ Branch 53 → 54 taken 13 times.
✗ Branch 53 → 72 not taken.
13 if (isPush(reader, u"vapid-public-key")) {
64 13 offer->vapidSeen = true;
65
2/2
✓ Branch 54 → 55 taken 13 times.
✓ Branch 60 → 61 taken 13 times.
26 offer->vapidType = reader.attributes().value(QStringLiteral("type")).toString();
66
2/2
✓ Branch 66 → 67 taken 13 times.
✓ Branch 67 → 68 taken 13 times.
13 offer->vapidText = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
67 } else {
68 ✗ reader.skipCurrentElement();
69 }
70 }
71 } else {
72 2 reader.skipCurrentElement(); // other (unknown) transports
73 }
74 }
75 149 return;
76 }
77
2/3
✓ Branch 88 → 89 taken 69 times.
✓ Branch 89 → 90 taken 69 times.
✗ Branch 89 → 127 not taken.
69 if (isPush(reader, u"supported-triggers")) {
78 69 offer->triggersSeen = true;
79
2/2
✓ Branch 125 → 91 taken 3 times.
✓ Branch 125 → 126 taken 69 times.
72 while (reader.readNextStartElement()) {
80
3/3
✓ Branch 100 → 101 taken 3 times.
✓ Branch 101 → 102 taken 1 time.
✓ Branch 101 → 123 taken 2 times.
3 if (isPush(reader, u"content-update")) {
81 1 int depth = 0;
82
3/3
✓ Branch 119 → 120 taken 2 times.
✓ Branch 120 → 103 taken 1 time.
✓ Branch 120 → 121 taken 1 time.
2 while (reader.readNextStartElement()) {
83
4/5
✓ Branch 112 → 113 taken 1 time.
✓ Branch 113 → 114 taken 1 time.
✗ Branch 113 → 118 not taken.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
1 if (isDav(reader, u"depth")) depth = parseDepth(reader.readElementText(QXmlStreamReader::SkipChildElements));
84 ✗ else reader.skipCurrentElement();
85 }
86 1 offer->contentUpdateDepth = qMax(offer->contentUpdateDepth, depth);
87 } else {
88 2 reader.skipCurrentElement();
89 }
90 }
91 69 return;
92 }
93 ✗ reader.skipCurrentElement();
94 }
95
96 322 DavPushOffer classify(const RawOffer &raw) {
97 322 DavPushOffer offer;
98
5/6
✓ Branch 2 → 3 taken 89 times.
✓ Branch 2 → 5 taken 233 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 89 times.
✓ Branch 7 → 8 taken 233 times.
✓ Branch 7 → 22 taken 89 times.
322 if (!raw.topicSeen || raw.topic.isEmpty()) {
99 233 offer.capability = DavPushCapability::Unsupported;
100
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 233 times.
699 offer.reason = raw.transportsSeen ? QStringLiteral("transports offered without a push topic")
101
2/4
✓ Branch 17 → 18 taken 233 times.
✗ Branch 17 → 19 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 233 times.
466 : QStringLiteral("no WebDAV-Push properties");
102 233 return offer;
103 }
104 89 offer.topic = raw.topic;
105
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 31 taken 88 times.
89 if (!raw.webPush) {
106 1 offer.capability = DavPushCapability::Incompatible;
107 1 offer.reason = QStringLiteral("no Web Push transport offered");
108 1 return offer;
109 }
110
2/2
✓ Branch 31 → 32 taken 13 times.
✓ Branch 31 → 67 taken 75 times.
88 if (raw.vapidSeen) {
111
1/1
✓ Branch 32 → 33 taken 13 times.
13 const auto key = WebPushPayloadCodec::decodeBase64Url(raw.vapidText);
112
3/4
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 38 taken 11 times.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 37 → 39 not taken.
13 const bool typeOk = raw.vapidType.isEmpty() || raw.vapidType == QLatin1String("p256ecdsa");
113
7/10
✓ Branch 40 → 41 taken 13 times.
✗ Branch 40 → 49 not taken.
✓ Branch 42 → 43 taken 13 times.
✗ Branch 42 → 49 not taken.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 45 → 49 taken 1 time.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 12 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 59 taken 12 times.
13 if (!typeOk || !key || key->size() != WebPushPayloadCodec::kPublicKeyBytes || key->at(0) != 0x04) {
114 1 offer.capability = DavPushCapability::Incompatible;
115 1 offer.reason = QStringLiteral("unusable VAPID public key");
116 1 return offer;
117 }
118 12 offer.vapidPublicKey = *key;
119 13 }
120
2/2
✓ Branch 67 → 68 taken 2 times.
✓ Branch 67 → 76 taken 85 times.
87 if (raw.triggersSeen) {
121 2 offer.profile = DavPushProfile::Draft;
122 // Members of a calendar change: content updates need depth >= 1.
123
2/2
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 77 taken 1 time.
2 if (raw.contentUpdateDepth < 1) {
124 1 offer.capability = DavPushCapability::Incompatible;
125 1 offer.reason = QStringLiteral("no content-update trigger with depth 1");
126 1 return offer;
127 }
128 } else {
129 85 offer.profile = DavPushProfile::NextcloudLegacy;
130 }
131 86 offer.capability = DavPushCapability::Supported;
132 86 return offer;
133 ✗ }
134
135 302 int statusCode(const QString &line) {
136
2/2
✓ Branch 2 → 3 taken 302 times.
✓ Branch 6 → 7 taken 302 times.
604 const QStringList parts = line.simplified().split(QLatin1Char(' '));
137
2/3
✓ Branch 9 → 10 taken 302 times.
✗ Branch 9 → 13 not taken.
✓ Branch 11 → 12 taken 302 times.
604 return parts.size() >= 2 ? parts.at(1).toInt() : 0;
138 302 }
139
140 // One DAV:propstat: its push properties and status.
141 391 void readPropstat(QXmlStreamReader &reader, RawOffer *merged) {
142 391 RawOffer found;
143 391 int status = 0;
144
3/3
✓ Branch 39 → 40 taken 1084 times.
✓ Branch 40 → 3 taken 693 times.
✓ Branch 40 → 41 taken 391 times.
1084 while (reader.readNextStartElement()) {
145
3/3
✓ Branch 12 → 13 taken 693 times.
✓ Branch 13 → 14 taken 391 times.
✓ Branch 13 → 23 taken 302 times.
693 if (isDav(reader, u"prop")) {
146
3/3
✓ Branch 21 → 22 taken 1920 times.
✓ Branch 22 → 15 taken 1529 times.
✓ Branch 22 → 39 taken 391 times.
1920 while (reader.readNextStartElement()) {
147
4/4
✓ Branch 16 → 17 taken 1529 times.
✓ Branch 18 → 19 taken 367 times.
✓ Branch 18 → 20 taken 1162 times.
✓ Branch 19 → 21 taken 367 times.
1529 if (reader.namespaceUri() == kNamespace) readPushProperty(reader, &found);
148
1/1
✓ Branch 20 → 21 taken 1162 times.
1162 else reader.skipCurrentElement();
149 }
150
2/3
✓ Branch 32 → 33 taken 302 times.
✓ Branch 33 → 34 taken 302 times.
✗ Branch 33 → 38 not taken.
302 } else if (isDav(reader, u"status")) {
151
2/2
✓ Branch 34 → 35 taken 302 times.
✓ Branch 35 → 36 taken 302 times.
302 status = statusCode(reader.readElementText(QXmlStreamReader::SkipChildElements));
152 } else {
153 ✗ reader.skipCurrentElement();
154 }
155 }
156 // S106-070: only a propstat whose DAV:status says 2xx counts — a missing
157 // or unparseable status (RFC 4918 makes it mandatory) used to be success.
158
4/4
✓ Branch 41 → 42 taken 302 times.
✓ Branch 41 → 43 taken 89 times.
✓ Branch 42 → 43 taken 68 times.
✓ Branch 42 → 44 taken 234 times.
391 if (status < 200 || status >= 300) return; // 404, missing, garbage: not offered
159
2/2
✓ Branch 44 → 45 taken 89 times.
✓ Branch 44 → 46 taken 145 times.
234 if (found.topicSeen) { merged->topicSeen = true; merged->topic = found.topic; }
160
2/2
✓ Branch 46 → 47 taken 89 times.
✓ Branch 46 → 49 taken 145 times.
234 if (found.transportsSeen) {
161 89 merged->transportsSeen = true;
162 89 merged->webPush = found.webPush;
163 89 merged->vapidSeen = found.vapidSeen;
164 89 merged->vapidText = found.vapidText;
165 89 merged->vapidType = found.vapidType;
166 }
167
2/2
✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 232 times.
234 if (found.triggersSeen) {
168 2 merged->triggersSeen = true;
169 2 merged->contentUpdateDepth = found.contentUpdateDepth;
170 }
171 391 }
172
173 } // namespace
174
175 219 QByteArray discoveryProperties() {
176 219 return "<P:transports/><P:topic/><P:supported-triggers/>";
177 }
178
179 193 std::optional<QHash<QString, DavPushOffer>> parseDiscovery(const QByteArray &multistatus, QString *error) {
180 // S106-071: the same DTD/entity refusal as parsePushMessage (defense in
181 // depth; QXmlStreamReader does not expand them, readNextStartElement
182 // skipped them silently).
183
4/6
✓ Branch 4 → 5 taken 192 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 7 → 8 taken 192 times.
✗ Branch 7 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 192 times.
385 if (multistatus.contains("<!DOCTYPE") || multistatus.contains("<!ENTITY") ||
184
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 21 taken 192 times.
385 multistatus.contains("<!doctype")) {
185 1 setError(error, QStringLiteral("DTDs and entities are not allowed"));
186 1 return std::nullopt;
187 }
188
1/1
✓ Branch 21 → 22 taken 192 times.
192 QXmlStreamReader reader(multistatus);
189 192 QHash<QString, DavPushOffer> offers;
190
7/8
✓ Branch 22 → 23 taken 192 times.
✓ Branch 23 → 24 taken 192 times.
✗ Branch 23 → 35 not taken.
✓ Branch 33 → 34 taken 192 times.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 36 taken 191 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 45 taken 191 times.
384 if (!reader.readNextStartElement() || !isDav(reader, u"multistatus")) {
191 1 setError(error, QStringLiteral("not a DAV multistatus"));
192 1 return std::nullopt;
193 }
194
3/3
✓ Branch 106 → 107 taken 515 times.
✓ Branch 107 → 46 taken 324 times.
✓ Branch 107 → 108 taken 191 times.
515 while (reader.readNextStartElement()) {
195
2/3
✓ Branch 55 → 56 taken 324 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 59 taken 324 times.
324 if (!isDav(reader, u"response")) {
196 ✗ reader.skipCurrentElement();
197 ✗ continue;
198 }
199 324 QString href;
200 324 RawOffer raw;
201
3/3
✓ Branch 95 → 96 taken 1037 times.
✓ Branch 96 → 60 taken 713 times.
✓ Branch 96 → 97 taken 324 times.
1037 while (reader.readNextStartElement()) {
202
8/9
✓ Branch 69 → 70 taken 713 times.
✓ Branch 70 → 71 taken 322 times.
✓ Branch 70 → 74 taken 391 times.
✓ Branch 72 → 73 taken 322 times.
✗ Branch 72 → 74 not taken.
✓ Branch 75 → 76 taken 322 times.
✓ Branch 75 → 82 taken 391 times.
✓ Branch 76 → 77 taken 322 times.
✓ Branch 77 → 78 taken 322 times.
713 if (isDav(reader, u"href") && href.isEmpty()) href = reader.readElementText().trimmed();
203
3/4
✓ Branch 91 → 92 taken 391 times.
✓ Branch 92 → 93 taken 391 times.
✗ Branch 92 → 94 not taken.
✓ Branch 93 → 95 taken 391 times.
391 else if (isDav(reader, u"propstat")) readPropstat(reader, &raw);
204 ✗ else reader.skipCurrentElement();
205 }
206
4/4
✓ Branch 98 → 99 taken 322 times.
✓ Branch 98 → 103 taken 2 times.
✓ Branch 99 → 100 taken 322 times.
✓ Branch 100 → 101 taken 322 times.
324 if (!href.isEmpty()) offers.insert(href, classify(raw));
207 324 }
208
3/3
✓ Branch 108 → 109 taken 191 times.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 122 taken 190 times.
191 if (reader.hasError()) {
209
2/2
✓ Branch 114 → 115 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
2 setError(error, QStringLiteral("invalid discovery XML: %1").arg(reader.errorString()));
210 1 return std::nullopt;
211 }
212 190 return offers;
213 192 }
214
215 83 QByteArray registerBody(const QString &pushResource, const QByteArray &publicKey, const QByteArray &authSecret) {
216 83 QByteArray body;
217
1/1
✓ Branch 2 → 3 taken 83 times.
83 QXmlStreamWriter writer(&body);
218
1/1
✓ Branch 3 → 4 taken 83 times.
83 writer.writeStartDocument();
219
1/1
✓ Branch 5 → 6 taken 83 times.
83 writer.writeDefaultNamespace(kNamespace);
220
1/1
✓ Branch 11 → 12 taken 83 times.
83 writer.writeNamespace(kDav, QStringLiteral("D"));
221
1/1
✓ Branch 19 → 20 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("push-register"));
222
1/1
✓ Branch 27 → 28 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("subscription"));
223
1/1
✓ Branch 35 → 36 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("web-push-subscription"));
224
1/1
✓ Branch 44 → 45 taken 83 times.
166 writer.writeTextElement(kNamespace, QStringLiteral("push-resource"), pushResource);
225
1/1
✓ Branch 56 → 57 taken 83 times.
166 writer.writeTextElement(kNamespace, QStringLiteral("content-encoding"), QStringLiteral("aes128gcm"));
226
1/1
✓ Branch 66 → 67 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("subscription-public-key"));
227
1/1
✓ Branch 77 → 78 taken 83 times.
166 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("p256dh"));
228
2/2
✓ Branch 82 → 83 taken 83 times.
✓ Branch 84 → 85 taken 83 times.
83 writer.writeCharacters(WebPushPayloadCodec::encodeBase64Url(publicKey));
229
1/1
✓ Branch 86 → 87 taken 83 times.
83 writer.writeEndElement();
230
2/2
✓ Branch 87 → 88 taken 83 times.
✓ Branch 94 → 95 taken 83 times.
166 writer.writeTextElement(kNamespace, QStringLiteral("auth-secret"), WebPushPayloadCodec::encodeBase64Url(authSecret));
231
1/1
✓ Branch 98 → 99 taken 83 times.
83 writer.writeEndElement(); // web-push-subscription
232
1/1
✓ Branch 99 → 100 taken 83 times.
83 writer.writeEndElement(); // subscription
233
1/1
✓ Branch 105 → 106 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("trigger"));
234
1/1
✓ Branch 113 → 114 taken 83 times.
83 writer.writeStartElement(kNamespace, QStringLiteral("content-update"));
235
1/1
✓ Branch 125 → 126 taken 83 times.
166 writer.writeTextElement(kDav, QStringLiteral("depth"), QStringLiteral("1"));
236
1/1
✓ Branch 130 → 131 taken 83 times.
83 writer.writeEndElement();
237
1/1
✓ Branch 131 → 132 taken 83 times.
83 writer.writeEndElement();
238
1/1
✓ Branch 132 → 133 taken 83 times.
83 writer.writeEndElement(); // push-register
239
1/1
✓ Branch 133 → 134 taken 83 times.
83 writer.writeEndDocument();
240 83 return body;
241 83 }
242
243 17 std::optional<PushMessage> parsePushMessage(const QByteArray &xml, QString *error) {
244
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 14 taken 16 times.
17 if (xml.size() > kMaxMessageBytes) {
245
1/1
✓ Branch 8 → 9 taken 1 time.
2 setError(error, QStringLiteral("push message exceeds %1 bytes").arg(kMaxMessageBytes));
246 1 return std::nullopt;
247 }
248
1/1
✓ Branch 14 → 15 taken 16 times.
16 QXmlStreamReader reader(xml);
249 16 PushMessage message;
250 16 int depth = 0;
251 16 bool rootSeen = false;
252 16 QStringList path;
253
3/3
✓ Branch 171 → 172 taken 140 times.
✓ Branch 172 → 16 taken 128 times.
✓ Branch 172 → 173 taken 12 times.
140 while (!reader.atEnd()) {
254
1/1
✓ Branch 16 → 17 taken 128 times.
128 const auto token = reader.readNext();
255
3/4
✓ Branch 17 → 18 taken 127 times.
✓ Branch 17 → 19 taken 1 time.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 26 taken 127 times.
128 if (token == QXmlStreamReader::DTD || token == QXmlStreamReader::EntityReference) {
256 1 setError(error, QStringLiteral("DTDs and entities are not allowed"));
257 1 return std::nullopt;
258 }
259
2/2
✓ Branch 26 → 27 taken 65 times.
✓ Branch 26 → 167 taken 62 times.
127 if (token == QXmlStreamReader::StartElement) {
260
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 35 taken 64 times.
65 if (++depth > kMaxDepth) {
261 1 setError(error, QStringLiteral("push message nested too deeply"));
262 1 return std::nullopt;
263 }
264
2/2
✓ Branch 35 → 36 taken 15 times.
✓ Branch 35 → 55 taken 49 times.
64 if (!rootSeen) {
265
3/3
✓ Branch 45 → 46 taken 15 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 54 taken 13 times.
15 if (!isPush(reader, u"push-message")) {
266 2 setError(error, QStringLiteral("root element is not push-message"));
267 2 return std::nullopt;
268 }
269 13 rootSeen = true;
270
7/7
✓ Branch 55 → 56 taken 24 times.
✓ Branch 55 → 68 taken 25 times.
✓ Branch 65 → 66 taken 24 times.
✓ Branch 66 → 67 taken 10 times.
✓ Branch 66 → 68 taken 14 times.
✓ Branch 69 → 70 taken 10 times.
✓ Branch 69 → 76 taken 39 times.
73 } else if (depth == 2 && isPush(reader, u"topic")) {
271
2/2
✓ Branch 70 → 71 taken 10 times.
✓ Branch 71 → 72 taken 10 times.
10 message.topic = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
272 10 --depth;
273 10 continue;
274
7/7
✓ Branch 76 → 77 taken 14 times.
✓ Branch 76 → 89 taken 25 times.
✓ Branch 86 → 87 taken 14 times.
✓ Branch 87 → 88 taken 9 times.
✓ Branch 87 → 89 taken 5 times.
✓ Branch 90 → 91 taken 9 times.
✓ Branch 90 → 92 taken 30 times.
53 } else if (depth == 2 && isPush(reader, u"content-update")) {
275 9 message.contentUpdate = true;
276
7/7
✓ Branch 92 → 93 taken 5 times.
✓ Branch 92 → 105 taken 25 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 103 → 104 taken 3 times.
✓ Branch 103 → 105 taken 2 times.
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 108 taken 27 times.
35 } else if (depth == 2 && isPush(reader, u"property-update")) {
277 3 message.propertyUpdate = true;
278
11/14
✓ Branch 108 → 109 taken 10 times.
✓ Branch 108 → 125 taken 17 times.
✓ Branch 110 → 111 taken 10 times.
✓ Branch 112 → 113 taken 7 times.
✓ Branch 112 → 125 taken 3 times.
✓ Branch 122 → 123 taken 7 times.
✓ Branch 123 → 124 taken 7 times.
✗ Branch 123 → 125 not taken.
✓ Branch 126 → 127 taken 10 times.
✓ Branch 126 → 128 taken 17 times.
✓ Branch 128 → 129 taken 7 times.
✓ Branch 128 → 135 taken 20 times.
✗ Branch 227 → 228 not taken.
✗ Branch 227 → 229 not taken.
34 } else if (depth == 3 && path.value(1) == QLatin1String("content-update") && isDav(reader, u"sync-token")) {
279
2/2
✓ Branch 129 → 130 taken 7 times.
✓ Branch 130 → 131 taken 7 times.
7 message.syncToken = reader.readElementText(QXmlStreamReader::SkipChildElements).trimmed();
280 7 --depth;
281 7 continue;
282
11/14
✓ Branch 135 → 136 taken 3 times.
✓ Branch 135 → 152 taken 17 times.
✓ Branch 137 → 138 taken 3 times.
✓ Branch 139 → 140 taken 2 times.
✓ Branch 139 → 152 taken 1 time.
✓ Branch 149 → 150 taken 2 times.
✓ Branch 150 → 151 taken 2 times.
✗ Branch 150 → 152 not taken.
✓ Branch 153 → 154 taken 3 times.
✓ Branch 153 → 155 taken 17 times.
✓ Branch 155 → 156 taken 2 times.
✓ Branch 155 → 157 taken 18 times.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
22 } else if (depth == 4 && path.value(1) == QLatin1String("property-update") && isPush(reader, u"transports")) {
283 2 message.transportsChanged = true;
284 }
285
1/1
✓ Branch 158 → 159 taken 45 times.
45 const bool own = reader.namespaceUri() == kNamespace;
286
5/5
✓ Branch 160 → 161 taken 42 times.
✓ Branch 160 → 163 taken 3 times.
✓ Branch 161 → 162 taken 42 times.
✓ Branch 162 → 164 taken 42 times.
✓ Branch 164 → 165 taken 45 times.
45 path.append(own ? reader.name().toString() : QString());
287
2/2
✓ Branch 167 → 168 taken 28 times.
✓ Branch 167 → 171 taken 34 times.
62 } else if (token == QXmlStreamReader::EndElement) {
288 28 --depth;
289
1/2
✓ Branch 169 → 170 taken 28 times.
✗ Branch 169 → 171 not taken.
28 if (!path.isEmpty()) path.removeLast();
290 }
291 }
292
6/7
✓ Branch 173 → 174 taken 12 times.
✓ Branch 174 → 175 taken 11 times.
✓ Branch 174 → 176 taken 1 time.
✗ Branch 175 → 176 not taken.
✓ Branch 175 → 177 taken 11 times.
✓ Branch 178 → 179 taken 1 time.
✓ Branch 178 → 191 taken 11 times.
12 if (reader.hasError() || !rootSeen) {
293
2/2
✓ Branch 183 → 184 taken 1 time.
✓ Branch 184 → 185 taken 1 time.
2 setError(error, QStringLiteral("invalid push message XML: %1").arg(reader.errorString()));
294 1 return std::nullopt;
295 }
296
6/6
✓ Branch 192 → 193 taken 3 times.
✓ Branch 192 → 195 taken 8 times.
✓ Branch 193 → 194 taken 1 time.
✓ Branch 193 → 195 taken 2 times.
✓ Branch 196 → 197 taken 1 time.
✓ Branch 196 → 204 taken 10 times.
11 if (message.topic.isEmpty() && !message.transportsChanged) {
297 1 setError(error, QStringLiteral("push message without topic"));
298 1 return std::nullopt;
299 }
300 10 return message;
301 16 }
302
303 140 std::optional<qint64> parseHttpDate(const QString &value) {
304 6 static const QRegularExpression pattern(QStringLiteral(
305
4/7
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 15 taken 137 times.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
146 R"(^(?:[A-Za-z]{3},\s*)?(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2}):(\d{2})\s+(GMT|UTC|Z|[+-]\d{4})\+?$)"));
306 3 static const QStringList months{QStringLiteral("jan"), QStringLiteral("feb"), QStringLiteral("mar"),
307 3 QStringLiteral("apr"), QStringLiteral("may"), QStringLiteral("jun"),
308 3 QStringLiteral("jul"), QStringLiteral("aug"), QStringLiteral("sep"),
309
5/10
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 73 taken 137 times.
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 73 not taken.
✓ Branch 58 → 59 taken 36 times.
✓ Branch 58 → 60 taken 3 times.
✗ Branch 168 → 169 not taken.
✗ Branch 168 → 170 not taken.
✗ Branch 213 → 214 not taken.
✗ Branch 213 → 215 not taken.
182 QStringLiteral("oct"), QStringLiteral("nov"), QStringLiteral("dec")};
310
2/2
✓ Branch 74 → 75 taken 140 times.
✓ Branch 75 → 76 taken 140 times.
140 const auto match = pattern.match(value.trimmed());
311
3/3
✓ Branch 77 → 78 taken 140 times.
✓ Branch 78 → 79 taken 68 times.
✓ Branch 78 → 82 taken 72 times.
140 if (!match.hasMatch()) return std::nullopt;
312
2/2
✓ Branch 82 → 83 taken 72 times.
✓ Branch 83 → 84 taken 72 times.
72 const int month = months.indexOf(match.captured(2).toLower()) + 1;
313
5/5
✓ Branch 87 → 88 taken 72 times.
✓ Branch 88 → 89 taken 72 times.
✓ Branch 89 → 90 taken 72 times.
✓ Branch 90 → 91 taken 72 times.
✓ Branch 91 → 92 taken 72 times.
72 const QDate date(match.captured(3).toInt(), month, match.captured(1).toInt());
314
7/7
✓ Branch 94 → 95 taken 72 times.
✓ Branch 95 → 96 taken 72 times.
✓ Branch 96 → 97 taken 72 times.
✓ Branch 97 → 98 taken 72 times.
✓ Branch 98 → 99 taken 72 times.
✓ Branch 99 → 100 taken 72 times.
✓ Branch 100 → 101 taken 72 times.
72 const QTime time(match.captured(4).toInt(), match.captured(5).toInt(), match.captured(6).toInt());
315
8/10
✓ Branch 104 → 105 taken 72 times.
✗ Branch 104 → 109 not taken.
✓ Branch 105 → 106 taken 72 times.
✓ Branch 106 → 107 taken 71 times.
✓ Branch 106 → 109 taken 1 time.
✓ Branch 107 → 108 taken 71 times.
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 71 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 115 taken 71 times.
72 if (month == 0 || !date.isValid() || !time.isValid()) return std::nullopt;
316 71 qint64 offsetSeconds = 0;
317
1/1
✓ Branch 115 → 116 taken 71 times.
71 const QString zone = match.captured(7);
318
7/8
✓ Branch 118 → 119 taken 71 times.
✓ Branch 119 → 120 taken 8 times.
✓ Branch 119 → 124 taken 63 times.
✓ Branch 122 → 123 taken 8 times.
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 125 taken 8 times.
✓ Branch 126 → 127 taken 63 times.
✓ Branch 126 → 145 taken 8 times.
71 if (zone.startsWith(QLatin1Char('+')) || zone.startsWith(QLatin1Char('-'))) {
319
2/2
✓ Branch 127 → 128 taken 63 times.
✓ Branch 128 → 129 taken 63 times.
63 const int hours = zone.mid(1, 2).toInt();
320
2/2
✓ Branch 130 → 131 taken 63 times.
✓ Branch 131 → 132 taken 63 times.
63 const int minutes = zone.mid(3, 2).toInt();
321
2/4
✓ Branch 133 → 134 taken 63 times.
✗ Branch 133 → 135 not taken.
✗ Branch 134 → 135 not taken.
✓ Branch 134 → 138 taken 63 times.
63 if (hours > 23 || minutes > 59) return std::nullopt;
322
2/3
✓ Branch 140 → 141 taken 63 times.
✗ Branch 141 → 142 not taken.
✓ Branch 141 → 143 taken 63 times.
63 offsetSeconds = (hours * 3600 + minutes * 60) * (zone.startsWith(QLatin1Char('-')) ? -1 : 1);
323 }
324
3/3
✓ Branch 145 → 146 taken 71 times.
✓ Branch 146 → 147 taken 71 times.
✓ Branch 147 → 148 taken 71 times.
71 return QDateTime(date, time, QTimeZone::utc()).toSecsSinceEpoch() - offsetSeconds;
325 179 }
326
327 } // namespace DavPushXml
328