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: 95.5% 442 / 0 / 463
Functions: 100.0% 33 / 0 / 33
Branches: 86.8% 387 / 0 / 446

data/SieveScript.cpp
Line Branch Exec Source
1 #include "SieveScript.h"
2
3 #include <QLoggingCategory>
4
5
4/7
✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 8 taken 62 times.
✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 10 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
72 Q_LOGGING_CATEGORY(lcSieveParse, "mailjd.sieve.parse")
6
7 namespace Sieve {
8
9 160108 QStringList Argument::values() const {
10 160108 QStringList result;
11
2/2
✓ Branch 8 → 4 taken 435784 times.
✓ Branch 8 → 9 taken 160108 times.
595892 for (const StringValue &string : strings)
12
1/1
✓ Branch 5 → 6 taken 435784 times.
435784 result.append(string.value);
13 160108 return result;
14 ✗ }
15
16 namespace {
17
18 enum class TokenKind {
19 Identifier,
20 Tag,
21 Number,
22 QuotedString,
23 MultiLineString,
24 LeftBracket,
25 RightBracket,
26 LeftParen,
27 RightParen,
28 LeftBrace,
29 RightBrace,
30 Comma,
31 Semicolon,
32 End,
33 Invalid,
34 };
35
36 struct Token {
37 TokenKind kind = TokenKind::End;
38 int begin = 0;
39 int end = 0;
40 QString text; // identifier/tag lower case, decoded string value, number text
41 quint64 number = 0;
42 QString error; // Invalid only
43 };
44
45 3310468 bool isAlpha(char c) {
46
5/10
✓ Branch 2 → 3 taken 2874311 times.
✓ Branch 2 → 4 taken 436157 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 2874311 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 436157 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 436157 times.
3310468 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
47 }
48
49
4/4
✓ Branch 2 → 3 taken 431189 times.
✓ Branch 2 → 5 taken 361082 times.
✓ Branch 3 → 4 taken 1347 times.
✓ Branch 3 → 5 taken 429842 times.
792271 bool isDigit(char c) { return c >= '0' && c <= '9'; }
50
51 // Scans tokens and skips whitespace, hash comments and bracket comments.
52 class Lexer {
53 public:
54 4204 explicit Lexer(const QByteArray &source) : m_source(source) {}
55
56 1644854 Token next() {
57 1644854 int errorPos = 0;
58
1/1
✓ Branch 2 → 3 taken 1644854 times.
1644854 const QString triviaError = skipTrivia(&errorPos);
59 1644854 Token token;
60 1644854 token.begin = m_pos;
61
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 10 taken 1644853 times.
1644854 if (!triviaError.isEmpty()) {
62 1 token.begin = errorPos;
63
1/1
✓ Branch 6 → 7 taken 1 time.
1 return invalid(token, triviaError);
64 }
65
2/2
✓ Branch 11 → 12 taken 4155 times.
✓ Branch 11 → 13 taken 1640698 times.
1644853 if (m_pos >= m_source.size()) {
66 4155 token.kind = TokenKind::End;
67 4155 token.end = m_pos;
68 4155 return token;
69 }
70 1640698 const char c = m_source.at(m_pos);
71
11/11
✓ Branch 14 → 15 taken 8561 times.
✓ Branch 14 → 19 taken 8562 times.
✓ Branch 14 → 23 taken 17619 times.
✓ Branch 14 → 27 taken 17619 times.
✓ Branch 14 → 31 taken 76135 times.
✓ Branch 14 → 35 taken 76048 times.
✓ Branch 14 → 39 taken 280427 times.
✓ Branch 14 → 43 taken 178839 times.
✓ Branch 14 → 47 taken 540092 times.
✓ Branch 14 → 52 taken 82019 times.
✓ Branch 14 → 57 taken 354777 times.
1640698 switch (c) {
72 8561 case '[': return single(token, TokenKind::LeftBracket);
73 8562 case ']': return single(token, TokenKind::RightBracket);
74 17619 case '(': return single(token, TokenKind::LeftParen);
75 17619 case ')': return single(token, TokenKind::RightParen);
76 76135 case '{': return single(token, TokenKind::LeftBrace);
77 76048 case '}': return single(token, TokenKind::RightBrace);
78 280427 case ',': return single(token, TokenKind::Comma);
79 178839 case ';': return single(token, TokenKind::Semicolon);
80
1/1
✓ Branch 48 → 49 taken 540092 times.
540092 case '"': return quotedString(token);
81
1/1
✓ Branch 53 → 54 taken 82019 times.
82019 case ':': return tag(token);
82 354777 default: break;
83 }
84
2/2
✓ Branch 59 → 60 taken 546 times.
✓ Branch 59 → 65 taken 354231 times.
354777 if (isDigit(c))
85
1/1
✓ Branch 61 → 62 taken 546 times.
546 return numberToken(token);
86
2/2
✓ Branch 66 → 67 taken 354221 times.
✓ Branch 66 → 72 taken 10 times.
354231 if (isAlpha(c))
87
1/1
✓ Branch 68 → 69 taken 354221 times.
354221 return identifier(token);
88 10 ++m_pos;
89
1/1
✓ Branch 76 → 77 taken 10 times.
10 return invalid(token, QStringLiteral("Unexpected character"));
90 1644854 }
91
92 int position() const { return m_pos; }
93 void setPosition(int pos) { m_pos = pos; }
94
95 private:
96 663810 Token single(Token token, TokenKind kind) {
97 663810 token.kind = kind;
98 663810 ++m_pos;
99 663810 token.end = m_pos;
100 663810 return token;
101 }
102
103 28 Token invalid(Token token, const QString &message) {
104 28 token.kind = TokenKind::Invalid;
105 28 token.end = qMax(m_pos, token.begin);
106 28 token.error = message;
107 28 return token;
108 }
109
110 1644854 QString skipTrivia(int *errorPos) {
111
2/2
✓ Branch 36 → 3 taken 2768228 times.
✓ Branch 36 → 37 taken 4155 times.
2772383 while (m_pos < m_source.size()) {
112 2768228 const char c = m_source.at(m_pos);
113
8/8
✓ Branch 4 → 5 taken 2168205 times.
✓ Branch 4 → 8 taken 600023 times.
✓ Branch 5 → 6 taken 2083118 times.
✓ Branch 5 → 8 taken 85087 times.
✓ Branch 6 → 7 taken 2082987 times.
✓ Branch 6 → 8 taken 131 times.
✓ Branch 7 → 8 taken 365115 times.
✓ Branch 7 → 9 taken 1717872 times.
2768228 if (c == ' ' || c == '\t' || c == '\r' || c == '\n') {
114 1050356 ++m_pos;
115
2/2
✓ Branch 9 → 10 taken 77046 times.
✓ Branch 9 → 16 taken 1640826 times.
1717872 } else if (c == '#') {
116 77046 const int lineEnd = m_source.indexOf('\n', m_pos);
117
2/2
✓ Branch 11 → 12 taken 21 times.
✓ Branch 11 → 14 taken 77025 times.
77046 m_pos = lineEnd < 0 ? m_source.size() : lineEnd + 1;
118
6/8
✓ Branch 16 → 17 taken 128 times.
✓ Branch 16 → 22 taken 1640698 times.
✓ Branch 18 → 19 taken 128 times.
✗ Branch 18 → 22 not taken.
✓ Branch 20 → 21 taken 128 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 128 times.
✓ Branch 23 → 34 taken 1640698 times.
1640954 } else if (c == '/' && m_pos + 1 < m_source.size() &&
119 128 m_source.at(m_pos + 1) == '*') {
120 128 const int close = m_source.indexOf("*/", m_pos + 2);
121
2/2
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 33 taken 127 times.
128 if (close < 0) {
122 1 *errorPos = m_pos;
123 1 m_pos = m_source.size();
124 1 return QStringLiteral("Unterminated bracket comment");
125 }
126 127 m_pos = close + 2;
127 } else {
128 1640698 break;
129 }
130 }
131 1644853 return {};
132 }
133
134 540092 Token quotedString(Token token) {
135 540092 QByteArray value;
136 540092 int pos = m_pos + 1;
137
2/2
✓ Branch 21 → 3 taken 4601631 times.
✓ Branch 21 → 22 taken 8 times.
4601639 while (pos < m_source.size()) {
138 4601631 const char c = m_source.at(pos);
139
2/2
✓ Branch 4 → 5 taken 540084 times.
✓ Branch 4 → 9 taken 4061547 times.
4601631 if (c == '"') {
140 540084 m_pos = pos + 1;
141 540084 token.kind = TokenKind::QuotedString;
142 540084 token.end = m_pos;
143
1/1
✓ Branch 5 → 6 taken 540084 times.
540084 token.text = QString::fromUtf8(value);
144 540084 return token;
145 }
146
5/6
✓ Branch 9 → 10 taken 5152 times.
✓ Branch 9 → 13 taken 4056395 times.
✓ Branch 11 → 12 taken 5152 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 5152 times.
✓ Branch 14 → 18 taken 4056395 times.
4061547 if (c == '\\' && pos + 1 < m_source.size()) {
147
1/1
✓ Branch 16 → 17 taken 5152 times.
5152 value.append(m_source.at(pos + 1));
148 5152 pos += 2;
149 5152 continue;
150 }
151
1/1
✓ Branch 18 → 19 taken 4056395 times.
4056395 value.append(c);
152 4056395 ++pos;
153 }
154 8 m_pos = m_source.size();
155
1/1
✓ Branch 27 → 28 taken 8 times.
8 return invalid(token, QStringLiteral("Unterminated string"));
156 540092 }
157
158 82019 Token tag(Token token) {
159 82019 int pos = m_pos + 1;
160
3/6
✓ Branch 3 → 4 taken 82019 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 82019 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 20 taken 82019 times.
82019 if (pos >= m_source.size() || !isAlpha(m_source.at(pos))) {
161 ✗ m_pos = pos;
162 ✗ return invalid(token, QStringLiteral("Expected a tag name after ':'"));
163 }
164
5/6
✓ Branch 23 → 24 taken 657209 times.
✗ Branch 23 → 31 not taken.
✓ Branch 26 → 27 taken 82019 times.
✓ Branch 26 → 30 taken 575190 times.
✓ Branch 32 → 21 taken 575190 times.
✓ Branch 32 → 33 taken 82019 times.
1396437 while (pos < m_source.size() &&
165
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 82019 times.
739228 (isAlpha(m_source.at(pos)) || isDigit(m_source.at(pos))))
166 575190 ++pos;
167 82019 token.kind = TokenKind::Tag;
168
3/3
✓ Branch 33 → 34 taken 82019 times.
✓ Branch 34 → 35 taken 82019 times.
✓ Branch 35 → 36 taken 82019 times.
82019 token.text = QString::fromLatin1(m_source.mid(m_pos, pos - m_pos)).toLower();
169 82019 m_pos = pos;
170 82019 token.end = pos;
171 82019 return token;
172 }
173
174 546 Token numberToken(Token token) {
175 546 int pos = m_pos;
176 546 quint64 value = 0;
177 546 bool overflow = false;
178
5/6
✓ Branch 8 → 9 taken 1347 times.
✗ Branch 8 → 13 not taken.
✓ Branch 11 → 12 taken 801 times.
✓ Branch 11 → 13 taken 546 times.
✓ Branch 14 → 3 taken 801 times.
✓ Branch 14 → 15 taken 546 times.
1347 while (pos < m_source.size() && isDigit(m_source.at(pos))) {
179 801 const quint64 digit = static_cast<quint64>(m_source.at(pos) - '0');
180
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 797 times.
801 if (value > (Q_UINT64_C(0xFFFFFFFFFFFFFFFF) - digit) / 10)
181 4 overflow = true;
182 801 value = value * 10 + digit;
183 801 ++pos;
184 }
185 546 quint64 multiplier = 1;
186
1/2
✓ Branch 16 → 17 taken 546 times.
✗ Branch 16 → 28 not taken.
546 if (pos < m_source.size()) {
187 546 const char unit = static_cast<char>(m_source.at(pos) | 0x20);
188
6/6
✓ Branch 18 → 19 taken 472 times.
✓ Branch 18 → 21 taken 74 times.
✓ Branch 19 → 20 taken 348 times.
✓ Branch 19 → 21 taken 124 times.
✓ Branch 20 → 21 taken 28 times.
✓ Branch 20 → 28 taken 320 times.
546 if (unit == 'k' || unit == 'm' || unit == 'g') {
189
4/4
✓ Branch 21 → 22 taken 152 times.
✓ Branch 21 → 26 taken 74 times.
✓ Branch 22 → 23 taken 124 times.
✓ Branch 22 → 24 taken 28 times.
226 multiplier = unit == 'k' ? 1024ULL : unit == 'm' ? 1024ULL * 1024 : 1024ULL * 1024 * 1024;
190 226 ++pos;
191 }
192 }
193 546 token.kind = TokenKind::Number;
194
2/2
✓ Branch 28 → 29 taken 546 times.
✓ Branch 29 → 30 taken 546 times.
546 token.text = QString::fromLatin1(m_source.mid(m_pos, pos - m_pos));
195
5/6
✓ Branch 33 → 34 taken 545 times.
✓ Branch 33 → 36 taken 1 time.
✓ Branch 34 → 35 taken 226 times.
✓ Branch 34 → 46 taken 319 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 46 taken 226 times.
546 if (overflow || (multiplier > 1 && value > Q_UINT64_C(0xFFFFFFFFFFFFFFFF) / multiplier)) {
196 1 m_pos = pos;
197
1/1
✓ Branch 40 → 41 taken 1 time.
1 return invalid(token, QStringLiteral("Number too large"));
198 }
199 545 token.number = value * multiplier;
200 545 m_pos = pos;
201 545 token.end = pos;
202 545 return token;
203 }
204
205 354221 Token identifier(Token token) {
206 354221 int pos = m_pos;
207
6/6
✓ Branch 5 → 6 taken 2217009 times.
✓ Branch 5 → 13 taken 93 times.
✓ Branch 8 → 9 taken 354128 times.
✓ Branch 8 → 12 taken 1862881 times.
✓ Branch 14 → 3 taken 1862881 times.
✓ Branch 14 → 15 taken 354221 times.
4788239 while (pos < m_source.size() &&
208
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 354128 times.
2571137 (isAlpha(m_source.at(pos)) || isDigit(m_source.at(pos))))
209 1862881 ++pos;
210
3/3
✓ Branch 15 → 16 taken 354221 times.
✓ Branch 16 → 17 taken 354221 times.
✓ Branch 17 → 18 taken 354221 times.
354221 const QString name = QString::fromLatin1(m_source.mid(m_pos, pos - m_pos)).toLower();
211
6/8
✓ Branch 22 → 23 taken 78 times.
✓ Branch 22 → 28 taken 354143 times.
✓ Branch 24 → 25 taken 78 times.
✗ Branch 24 → 28 not taken.
✓ Branch 26 → 27 taken 78 times.
✗ Branch 26 → 28 not taken.
✓ Branch 29 → 30 taken 78 times.
✓ Branch 29 → 35 taken 354143 times.
354299 if (name == QLatin1String("text") && pos < m_source.size() &&
212 78 m_source.at(pos) == ':') {
213 78 m_pos = pos + 1;
214
1/1
✓ Branch 31 → 32 taken 78 times.
78 return multiLine(token);
215 }
216 354143 token.kind = TokenKind::Identifier;
217 354143 token.text = name;
218 354143 m_pos = pos;
219 354143 token.end = pos;
220 354143 return token;
221 354221 }
222
223 // RFC 5228 §2.4.2: "text:" *(SP / HTAB) (hash-comment / CRLF), then lines
224 // up to a line that is a single ".". Bare LF line ends are accepted.
225 78 Token multiLine(Token token) {
226 78 int pos = m_pos;
227
6/8
✓ Branch 5 → 6 taken 80 times.
✗ Branch 5 → 11 not taken.
✓ Branch 7 → 8 taken 78 times.
✓ Branch 7 → 10 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 78 times.
✓ Branch 12 → 3 taken 2 times.
✓ Branch 12 → 13 taken 78 times.
80 while (pos < m_source.size() && (m_source.at(pos) == ' ' || m_source.at(pos) == '\t'))
228 2 ++pos;
229
5/6
✓ Branch 14 → 15 taken 78 times.
✗ Branch 14 → 18 not taken.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 76 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 26 taken 76 times.
78 if (pos < m_source.size() && m_source.at(pos) == '#') {
230 2 const int lineEnd = m_source.indexOf('\n', pos);
231
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 2 times.
2 pos = lineEnd < 0 ? m_source.size() : lineEnd + 1;
232
3/4
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 35 taken 72 times.
✓ Branch 31 → 32 taken 4 times.
✗ Branch 31 → 35 not taken.
156 } else if (pos < m_source.size() && m_source.at(pos) == '\r' &&
233
4/6
✓ Branch 27 → 28 taken 76 times.
✗ Branch 27 → 35 not taken.
✓ Branch 33 → 34 taken 4 times.
✗ Branch 33 → 35 not taken.
✓ Branch 36 → 37 taken 4 times.
✓ Branch 36 → 38 taken 72 times.
156 pos + 1 < m_source.size() && m_source.at(pos + 1) == '\n') {
234 4 pos += 2;
235
3/6
✓ Branch 39 → 40 taken 72 times.
✗ Branch 39 → 43 not taken.
✓ Branch 41 → 42 taken 72 times.
✗ Branch 41 → 43 not taken.
✓ Branch 44 → 45 taken 72 times.
✗ Branch 44 → 46 not taken.
72 } else if (pos < m_source.size() && m_source.at(pos) == '\n') {
236 72 pos += 1;
237 } else {
238 ✗ m_pos = pos;
239 ✗ return invalid(token, QStringLiteral("Expected a line break after 'text:'"));
240 }
241 78 QByteArray value;
242
2/2
✓ Branch 96 → 57 taken 218 times.
✓ Branch 96 → 97 taken 8 times.
226 while (pos < m_source.size()) {
243 218 int lineEnd = m_source.indexOf('\n', pos);
244
2/2
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 69 taken 217 times.
218 if (lineEnd < 0) {
245 // A final "." without a line break still ends the string.
246
2/3
✓ Branch 59 → 60 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
✗ Branch 62 → 68 not taken.
1 if (m_source.mid(pos) == ".") {
247 1 m_pos = m_source.size();
248 1 token.kind = TokenKind::MultiLineString;
249 1 token.end = m_pos;
250
1/1
✓ Branch 64 → 65 taken 1 time.
1 token.text = QString::fromUtf8(value);
251 70 return token;
252 }
253 ✗ break;
254 }
255
1/1
✓ Branch 69 → 70 taken 217 times.
217 QByteArray line = m_source.mid(pos, lineEnd - pos);
256
1/1
✓ Branch 70 → 71 taken 217 times.
217 const bool crlf = line.endsWith('\r');
257
2/2
✓ Branch 71 → 72 taken 13 times.
✓ Branch 71 → 73 taken 204 times.
217 if (crlf)
258
1/1
✓ Branch 72 → 73 taken 13 times.
13 line.chop(1);
259
2/2
✓ Branch 74 → 75 taken 69 times.
✓ Branch 74 → 80 taken 148 times.
217 if (line == ".") {
260 69 m_pos = lineEnd + 1;
261 69 token.kind = TokenKind::MultiLineString;
262 69 token.end = m_pos;
263
1/1
✓ Branch 75 → 76 taken 69 times.
69 token.text = QString::fromUtf8(value);
264 69 return token;
265 }
266
2/2
✓ Branch 82 → 83 taken 59 times.
✓ Branch 82 → 84 taken 89 times.
148 if (line.startsWith(".."))
267
1/1
✓ Branch 83 → 84 taken 59 times.
59 line.remove(0, 1);
268
1/1
✓ Branch 84 → 85 taken 148 times.
148 value.append(line);
269
3/3
✓ Branch 85 → 86 taken 8 times.
✓ Branch 85 → 87 taken 140 times.
✓ Branch 88 → 89 taken 148 times.
148 value.append(crlf ? "\r\n" : "\n");
270 148 pos = lineEnd + 1;
271 217 }
272 8 m_pos = m_source.size();
273
1/1
✓ Branch 102 → 103 taken 8 times.
8 return invalid(token, QStringLiteral("Unterminated multi-line string"));
274 78 }
275
276 const QByteArray &m_source;
277 int m_pos = 0;
278 };
279
280 class Parser {
281 public:
282 4204 Parser(const QByteArray &source, const Limits &limits)
283 4204 : m_source(source), m_lexer(source), m_limits(limits) {
284
1/1
✓ Branch 5 → 6 taken 4204 times.
4204 advance();
285 4204 }
286
287 3638 void parseScript(Script *script) {
288
6/6
✓ Branch 13 → 14 taken 108887 times.
✓ Branch 13 → 17 taken 3567 times.
✓ Branch 15 → 16 taken 108886 times.
✓ Branch 15 → 17 taken 1 time.
✓ Branch 18 → 3 taken 108886 times.
✓ Branch 18 → 19 taken 3568 times.
112454 while (m_token.kind != TokenKind::End && m_error.isNull()) {
289 108886 Command command;
290
3/3
✓ Branch 3 → 4 taken 108886 times.
✓ Branch 4 → 5 taken 70 times.
✓ Branch 4 → 6 taken 108816 times.
108886 if (!parseCommand(&command, 0))
291 70 break;
292
1/1
✓ Branch 6 → 7 taken 108816 times.
108816 script->commands.append(command);
293 108886 }
294 3638 script->error = m_error;
295 3638 }
296
297 566 bool parseSingleTest(Test *test) {
298
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 565 times.
566 if (m_token.kind != TokenKind::Identifier)
299 1 return false;
300
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 565 times.
565 if (!parseTest(test, 0))
301 ✗ return false;
302 565 return m_token.kind == TokenKind::End;
303 }
304
305 private:
306 1644854 void advance() {
307 1644854 m_previousEnd = m_token.end;
308
1/1
✓ Branch 2 → 3 taken 1644854 times.
1644854 m_token = m_lexer.next();
309
2/2
✓ Branch 5 → 6 taken 28 times.
✓ Branch 5 → 7 taken 1644826 times.
1644854 if (m_token.kind == TokenKind::Invalid)
310 28 fail(m_token.begin, m_token.error);
311 1644854 }
312
313 71 bool fail(int offset, const QString &message) {
314
1/2
✓ Branch 3 → 4 taken 71 times.
✗ Branch 3 → 8 not taken.
71 if (m_error.isNull()) {
315 71 m_error.message = message;
316 71 m_error.offset = offset;
317 71 m_error.line = lineOfOffset(m_source, offset);
318 71 m_error.column = columnOfOffset(m_source, offset);
319 }
320 71 return false;
321 }
322
323 255011 bool parseCommand(Command *command, int depth) {
324
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 11 taken 255011 times.
255011 if (depth > m_limits.maxDepth)
325 ✗ return fail(m_token.begin, QStringLiteral("Nesting too deep"));
326
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 20 taken 255010 times.
255011 if (++m_commandCount > m_limits.maxCommands)
327
1/1
✓ Branch 15 → 16 taken 1 time.
1 return fail(m_token.begin, QStringLiteral("Too many commands"));
328
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 29 taken 255010 times.
255010 if (m_token.kind != TokenKind::Identifier)
329 ✗ return fail(m_token.begin, QStringLiteral("Expected a command"));
330 255010 command->name = m_token.text;
331 255010 command->span.begin = m_token.begin;
332 255010 command->line = lineAt(m_token.begin);
333 255010 advance();
334
2/2
✓ Branch 33 → 34 taken 28 times.
✓ Branch 33 → 35 taken 254982 times.
255010 if (!parseArguments(&command->arguments, &command->tests, &command->testList, depth))
335 28 return false;
336
2/2
✓ Branch 35 → 36 taken 178838 times.
✓ Branch 35 → 38 taken 76144 times.
254982 if (m_token.kind == TokenKind::Semicolon) {
337 178838 command->span.end = m_token.end;
338 178838 advance();
339 178838 return m_error.isNull();
340 }
341
2/2
✓ Branch 38 → 39 taken 9 times.
✓ Branch 38 → 50 taken 76135 times.
76144 if (m_token.kind != TokenKind::LeftBrace)
342
1/1
✓ Branch 44 → 45 taken 9 times.
27 return fail(m_token.begin, QStringLiteral("Expected ';' or '{' after \"%1\"")
343
1/1
✓ Branch 43 → 44 taken 9 times.
27 .arg(command->name));
344 76135 command->hasBlock = true;
345 76135 command->blockSpan.begin = m_token.begin;
346 76135 advance();
347
2/2
✓ Branch 74 → 51 taken 146148 times.
✓ Branch 74 → 75 taken 76040 times.
222188 while (m_token.kind != TokenKind::RightBrace) {
348
2/2
✓ Branch 51 → 52 taken 23 times.
✓ Branch 51 → 60 taken 146125 times.
146148 if (m_token.kind == TokenKind::End)
349
1/1
✓ Branch 55 → 56 taken 23 times.
23 return fail(m_token.begin, QStringLiteral("Missing '}'"));
350
1/2
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 146125 times.
146125 if (!m_error.isNull())
351 ✗ return false;
352 146125 Command child;
353
3/3
✓ Branch 63 → 64 taken 146125 times.
✓ Branch 64 → 65 taken 72 times.
✓ Branch 64 → 66 taken 146053 times.
146125 if (!parseCommand(&child, depth + 1))
354 72 return false;
355
1/1
✓ Branch 66 → 67 taken 146053 times.
146053 command->block.append(child);
356 146125 }
357 76040 command->blockSpan.end = m_token.end;
358 76040 command->span.end = m_token.end;
359 76040 advance();
360 76040 return m_error.isNull();
361 }
362
363 354140 bool parseArguments(QList<Argument> *arguments, QList<Test> *tests, bool *testList,
364 int depth) {
365
2/2
✓ Branch 37 → 3 taken 701029 times.
✓ Branch 37 → 38 taken 18 times.
701047 while (m_error.isNull()) {
366
2/2
✓ Branch 3 → 4 taken 82019 times.
✓ Branch 3 → 9 taken 619010 times.
701029 if (m_token.kind == TokenKind::Tag) {
367 82019 Argument argument;
368 82019 argument.kind = Argument::Kind::Tag;
369 82019 argument.span = {m_token.begin, m_token.end};
370 82019 argument.text = m_token.text;
371
1/1
✓ Branch 5 → 6 taken 82019 times.
82019 arguments->append(argument);
372
1/1
✓ Branch 6 → 7 taken 82019 times.
82019 advance();
373
2/2
✓ Branch 9 → 10 taken 545 times.
✓ Branch 9 → 15 taken 618465 times.
701029 } else if (m_token.kind == TokenKind::Number) {
374 545 Argument argument;
375 545 argument.kind = Argument::Kind::Number;
376 545 argument.span = {m_token.begin, m_token.end};
377 545 argument.text = m_token.text;
378 545 argument.number = m_token.number;
379
1/1
✓ Branch 11 → 12 taken 545 times.
545 arguments->append(argument);
380
1/1
✓ Branch 12 → 13 taken 545 times.
545 advance();
381
2/2
✓ Branch 15 → 16 taken 362753 times.
✓ Branch 15 → 17 taken 255712 times.
619010 } else if (m_token.kind == TokenKind::QuotedString ||
382
2/2
✓ Branch 16 → 17 taken 70 times.
✓ Branch 16 → 24 taken 362683 times.
362753 m_token.kind == TokenKind::MultiLineString) {
383 255782 Argument argument;
384 255782 argument.kind = Argument::Kind::StringList;
385 255782 argument.span = {m_token.begin, m_token.end};
386
1/1
✓ Branch 18 → 19 taken 255782 times.
255782 argument.strings.append(stringValue());
387
1/1
✓ Branch 20 → 21 taken 255782 times.
255782 arguments->append(argument);
388
1/1
✓ Branch 21 → 22 taken 255782 times.
255782 advance();
389
2/2
✓ Branch 24 → 25 taken 8561 times.
✓ Branch 24 → 35 taken 354122 times.
618465 } else if (m_token.kind == TokenKind::LeftBracket) {
390 8561 Argument argument;
391
2/3
✓ Branch 25 → 26 taken 8561 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 8561 times.
8561 if (!parseStringList(&argument))
392 ✗ return false;
393
1/1
✓ Branch 28 → 29 taken 8561 times.
8561 arguments->append(argument);
394
1/2
✓ Branch 31 → 32 taken 8561 times.
✗ Branch 31 → 34 not taken.
8561 } else {
395 354122 break;
396 }
397 }
398
2/2
✓ Branch 39 → 40 taken 18 times.
✓ Branch 39 → 41 taken 354122 times.
354140 if (!m_error.isNull())
399 18 return false;
400
2/2
✓ Branch 41 → 42 taken 76340 times.
✓ Branch 41 → 49 taken 277782 times.
354122 if (m_token.kind == TokenKind::Identifier) {
401 76340 Test test;
402
3/3
✓ Branch 42 → 43 taken 76340 times.
✓ Branch 43 → 44 taken 81 times.
✓ Branch 43 → 45 taken 76259 times.
76340 if (!parseTest(&test, depth + 1))
403 81 return false;
404
1/1
✓ Branch 45 → 46 taken 76259 times.
76259 tests->append(test);
405 76259 return true;
406 76340 }
407
2/2
✓ Branch 49 → 50 taken 17619 times.
✓ Branch 49 → 87 taken 260163 times.
277782 if (m_token.kind == TokenKind::LeftParen) {
408 17619 *testList = true;
409 17619 advance();
410
1/2
✓ Branch 85 → 52 taken 22235 times.
✗ Branch 85 → 86 not taken.
22235 while (m_error.isNull()) {
411 22235 Test test;
412
2/2
✓ Branch 52 → 53 taken 8 times.
✓ Branch 52 → 61 taken 22227 times.
22235 if (m_token.kind != TokenKind::Identifier)
413
1/1
✓ Branch 56 → 57 taken 8 times.
8 return fail(m_token.begin, QStringLiteral("Expected a test"));
414
2/3
✓ Branch 61 → 62 taken 22227 times.
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 22227 times.
22227 if (!parseTest(&test, depth + 1))
415 ✗ return false;
416
1/1
✓ Branch 64 → 65 taken 22227 times.
22227 tests->append(test);
417
2/2
✓ Branch 65 → 66 taken 4616 times.
✓ Branch 65 → 68 taken 17611 times.
22227 if (m_token.kind == TokenKind::Comma) {
418
1/1
✓ Branch 66 → 67 taken 4616 times.
4616 advance();
419 4616 continue;
420 }
421
1/2
✓ Branch 68 → 69 taken 17611 times.
✗ Branch 68 → 72 not taken.
17611 if (m_token.kind == TokenKind::RightParen) {
422
1/1
✓ Branch 69 → 70 taken 17611 times.
17611 advance();
423 17611 return m_error.isNull();
424 }
425 ✗ return fail(m_token.begin, QStringLiteral("Expected ',' or ')' in test list"));
426 22235 }
427 ✗ return false;
428 }
429 260163 return true;
430 }
431
432 8561 bool parseStringList(Argument *argument) {
433 8561 argument->kind = Argument::Kind::StringList;
434 8561 argument->bracketed = true;
435 8561 argument->span.begin = m_token.begin;
436 8561 advance();
437
1/2
✓ Branch 33 → 3 taken 284372 times.
✗ Branch 33 → 34 not taken.
284372 while (m_error.isNull()) {
438
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 13 taken 284372 times.
284372 if (m_token.kind != TokenKind::QuotedString &&
439 ✗ m_token.kind != TokenKind::MultiLineString)
440 ✗ return fail(m_token.begin, QStringLiteral("Expected a string in string list"));
441
1/1
✓ Branch 14 → 15 taken 284372 times.
284372 argument->strings.append(stringValue());
442 284372 advance();
443
2/2
✓ Branch 17 → 18 taken 275811 times.
✓ Branch 17 → 20 taken 8561 times.
284372 if (m_token.kind == TokenKind::Comma) {
444 275811 advance();
445 275811 continue;
446 }
447
1/2
✓ Branch 20 → 21 taken 8561 times.
✗ Branch 20 → 23 not taken.
8561 if (m_token.kind == TokenKind::RightBracket) {
448 8561 argument->span.end = m_token.end;
449 8561 advance();
450 8561 return m_error.isNull();
451 }
452 ✗ return fail(m_token.begin, QStringLiteral("Expected ',' or ']' in string list"));
453 }
454 ✗ return false;
455 }
456
457 99132 bool parseTest(Test *test, int depth) {
458
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 11 taken 99130 times.
99132 if (depth > m_limits.maxDepth)
459
1/1
✓ Branch 6 → 7 taken 2 times.
2 return fail(m_token.begin, QStringLiteral("Nesting too deep"));
460 99130 test->name = m_token.text;
461 99130 test->span.begin = m_token.begin;
462
1/1
✓ Branch 12 → 13 taken 99130 times.
99130 test->line = lineAt(m_token.begin);
463 99130 int end = m_token.end;
464
1/1
✓ Branch 13 → 14 taken 99130 times.
99130 advance();
465
3/3
✓ Branch 14 → 15 taken 99130 times.
✓ Branch 15 → 16 taken 79 times.
✓ Branch 15 → 17 taken 99051 times.
99130 if (!parseArguments(&test->arguments, &test->tests, &test->testList, depth))
466 79 return false;
467 // The test ends where the last consumed token ended.
468 99051 test->span.end = qMax(end, m_previousEnd);
469 99051 return true;
470 }
471
472 // Line of `offset`. Commands and tests are met in source order, so the
473 // count resumes where the previous call stopped: linear in the script
474 // size instead of quadratic (counting from the start for every command).
475 354140 int lineAt(int offset) {
476
1/1
✓ Branch 3 → 4 taken 354140 times.
354140 offset = qBound(0, offset, static_cast<int>(m_source.size()));
477
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 354140 times.
354140 if (offset < m_lineOffset) {
478 ✗ m_lineOffset = 0;
479 ✗ m_line = 1;
480 }
481
2/2
✓ Branch 11 → 7 taken 9075715 times.
✓ Branch 11 → 12 taken 354140 times.
9429855 for (int i = m_lineOffset; i < offset; ++i)
482
2/2
✓ Branch 8 → 9 taken 436204 times.
✓ Branch 8 → 10 taken 8639511 times.
9075715 if (m_source.at(i) == '\n')
483 436204 ++m_line;
484 354140 m_lineOffset = offset;
485 354140 return m_line;
486 }
487
488 540154 StringValue stringValue() const {
489 540154 StringValue value;
490 540154 value.value = m_token.text;
491 540154 value.span = {m_token.begin, m_token.end};
492 540154 value.multiLine = m_token.kind == TokenKind::MultiLineString;
493 540154 return value;
494 }
495
496 const QByteArray &m_source;
497 Lexer m_lexer;
498 Limits m_limits;
499 Token m_token;
500 int m_previousEnd = 0;
501 int m_commandCount = 0;
502 int m_lineOffset = 0;
503 int m_line = 1;
504 ParseError m_error;
505 };
506
507 } // namespace
508
509 3639 Script parse(const QByteArray &source, const Limits &limits) {
510 3639 Script script;
511 3639 script.source = source;
512
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 25 taken 3638 times.
3639 if (source.size() > limits.maxBytes) {
513 1 script.error.message = QStringLiteral("Script too large");
514 1 script.error.offset = 0;
515 1 script.error.line = 1;
516 1 script.error.column = 1;
517
7/7
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 23 → 13 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
2 qCInfo(lcSieveParse) << "Refused to parse a script of" << source.size() << "bytes";
518 1 return script;
519 }
520
1/1
✓ Branch 25 → 26 taken 3638 times.
3638 Parser parser(script.source, limits);
521
1/1
✓ Branch 26 → 27 taken 3638 times.
3638 parser.parseScript(&script);
522
2/2
✓ Branch 28 → 29 taken 71 times.
✓ Branch 28 → 48 taken 3567 times.
3638 if (!script.isValid())
523
7/7
✓ Branch 29 → 30 taken 71 times.
✓ Branch 33 → 34 taken 65 times.
✓ Branch 35 → 36 taken 65 times.
✓ Branch 36 → 37 taken 65 times.
✓ Branch 37 → 38 taken 65 times.
✓ Branch 46 → 31 taken 65 times.
✓ Branch 46 → 47 taken 71 times.
136 qCInfo(lcSieveParse).nospace() << "Parse error at " << script.error.line << ':'
524
3/3
✓ Branch 38 → 39 taken 65 times.
✓ Branch 39 → 40 taken 65 times.
✓ Branch 41 → 42 taken 65 times.
65 << script.error.column << " (" << source.size()
525
1/1
✓ Branch 42 → 43 taken 65 times.
65 << " bytes)";
526 3638 return script;
527 3638 }
528
529 566 bool parseTestExpression(const QByteArray &text, Test *test) {
530 566 Limits limits;
531
1/1
✓ Branch 2 → 3 taken 566 times.
566 Parser parser(text, limits);
532
1/1
✓ Branch 3 → 4 taken 566 times.
1132 return parser.parseSingleTest(test);
533 566 }
534
535 66 QByteArray serialize(const Script &script) {
536 66 QByteArray out;
537
1/1
✓ Branch 3 → 4 taken 66 times.
66 out.reserve(script.source.size());
538 66 int pos = 0;
539
2/2
✓ Branch 16 → 6 taken 216 times.
✓ Branch 16 → 17 taken 66 times.
282 for (const Command &command : script.commands) {
540
2/2
✓ Branch 7 → 8 taken 216 times.
✓ Branch 8 → 9 taken 216 times.
216 out.append(script.source.mid(pos, command.span.begin - pos));
541
2/2
✓ Branch 11 → 12 taken 216 times.
✓ Branch 12 → 13 taken 216 times.
216 out.append(script.source.mid(command.span.begin, command.span.length()));
542 216 pos = command.span.end;
543 }
544
2/2
✓ Branch 17 → 18 taken 66 times.
✓ Branch 18 → 19 taken 66 times.
66 out.append(script.source.mid(pos));
545 66 return out;
546 ✗ }
547
548 71 int lineOfOffset(const QByteArray &source, int offset) {
549
1/1
✓ Branch 3 → 4 taken 71 times.
71 const int limit = qBound(0, offset, static_cast<int>(source.size()));
550 71 int line = 1;
551
2/2
✓ Branch 9 → 5 taken 56640 times.
✓ Branch 9 → 10 taken 71 times.
56711 for (int i = 0; i < limit; ++i)
552
2/2
✓ Branch 6 → 7 taken 285 times.
✓ Branch 6 → 8 taken 56355 times.
56640 if (source.at(i) == '\n')
553 285 ++line;
554 71 return line;
555 }
556
557 71 int columnOfOffset(const QByteArray &source, int offset) {
558
1/1
✓ Branch 3 → 4 taken 71 times.
71 const int limit = qBound(0, offset, static_cast<int>(source.size()));
559 // S106-090: lastIndexOf(…, -1) would search from the end of the source.
560
2/2
✓ Branch 4 → 5 taken 70 times.
✓ Branch 4 → 7 taken 1 time.
71 const int lineStart = limit > 0 ? source.lastIndexOf('\n', limit - 1) + 1 : 0;
561
2/2
✓ Branch 8 → 9 taken 71 times.
✓ Branch 9 → 10 taken 71 times.
71 return static_cast<int>(QString::fromUtf8(source.mid(lineStart, limit - lineStart)).size()) + 1;
562 }
563
564 398284 bool representableString(const QString &value) {
565
2/2
✓ Branch 25 → 3 taken 2307078 times.
✓ Branch 25 → 26 taken 398280 times.
2705358 for (int i = 0; i < value.size(); ++i) {
566 2307078 const QChar c = value.at(i);
567
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 2307077 times.
2307078 if (c.unicode() == 0)
568 4 return false;
569
8/8
✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 19 taken 2307071 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 18 taken 1 time.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 22 taken 2307074 times.
2307077 if (c == QLatin1Char('\r') && (i + 1 >= value.size() || value.at(i + 1) != QLatin1Char('\n')))
570 3 return false;
571 }
572 398280 return true;
573 }
574
575 2937 QByteArray stringLiteral(const QString &value, const QByteArray &lineEnd) {
576 2937 QString body = value;
577
1/1
✓ Branch 9 → 10 taken 2937 times.
5874 body.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
578
1/1
✓ Branch 18 → 19 taken 2937 times.
2937 body.replace(QLatin1Char('\r'), QLatin1Char('\n'));
579
1/1
✓ Branch 20 → 21 taken 2937 times.
2937 body.remove(QChar(0));
580
3/3
✓ Branch 23 → 24 taken 2937 times.
✓ Branch 24 → 25 taken 22 times.
✓ Branch 24 → 50 taken 2915 times.
2937 if (body.endsWith(QLatin1Char('\n'))) {
581
2/5
✓ Branch 25 → 26 taken 22 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 22 times.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
44 QByteArray out = "text:" + lineEnd;
582
1/1
✓ Branch 26 → 27 taken 22 times.
22 body.chop(1);
583
1/1
✓ Branch 30 → 31 taken 22 times.
22 const QStringList lines = body.split(QLatin1Char('\n'));
584
2/2
✓ Branch 43 → 33 taken 45 times.
✓ Branch 43 → 44 taken 22 times.
67 for (const QString &line : lines) {
585
1/1
✓ Branch 34 → 35 taken 45 times.
45 QByteArray bytes = line.toUtf8();
586
3/3
✓ Branch 35 → 36 taken 45 times.
✓ Branch 36 → 37 taken 17 times.
✓ Branch 36 → 38 taken 28 times.
45 if (bytes.startsWith('.'))
587
1/1
✓ Branch 37 → 38 taken 17 times.
17 bytes.prepend('.');
588
1/1
✓ Branch 38 → 39 taken 45 times.
45 out.append(bytes);
589
1/1
✓ Branch 39 → 40 taken 45 times.
45 out.append(lineEnd);
590 45 }
591
1/1
✓ Branch 44 → 45 taken 22 times.
22 out.append('.');
592
1/1
✓ Branch 45 → 46 taken 22 times.
22 out.append(lineEnd);
593 22 return out;
594 22 }
595
1/1
✓ Branch 50 → 51 taken 2915 times.
2915 QByteArray out = "\"";
596
1/1
✓ Branch 51 → 52 taken 2915 times.
2915 const QByteArray bytes = body.toUtf8();
597
2/2
✓ Branch 62 → 54 taken 27423 times.
✓ Branch 62 → 63 taken 2915 times.
30338 for (const char c : bytes) {
598
2/2
✓ Branch 54 → 55 taken 6 times.
✓ Branch 54 → 57 taken 27417 times.
27423 if (c == '\n') {
599
1/1
✓ Branch 55 → 56 taken 6 times.
6 out.append(lineEnd); // quoted strings may span lines (RFC 5228 §2.4.2)
600 6 continue;
601 }
602
4/4
✓ Branch 57 → 58 taken 27401 times.
✓ Branch 57 → 59 taken 16 times.
✓ Branch 58 → 59 taken 123 times.
✓ Branch 58 → 60 taken 27278 times.
27417 if (c == '"' || c == '\\')
603
1/1
✓ Branch 59 → 60 taken 139 times.
139 out.append('\\');
604
1/1
✓ Branch 60 → 61 taken 27417 times.
27417 out.append(c);
605 }
606
1/1
✓ Branch 63 → 64 taken 2915 times.
2915 out.append('"');
607 2915 return out;
608 2937 }
609
610 1739 QByteArray stringListLiteral(const QStringList &values, const QByteArray &lineEnd) {
611
2/2
✓ Branch 3 → 4 taken 1667 times.
✓ Branch 3 → 7 taken 72 times.
1739 if (values.size() == 1)
612
1/1
✓ Branch 5 → 6 taken 1667 times.
1667 return stringLiteral(values.first(), lineEnd);
613
1/1
✓ Branch 7 → 8 taken 72 times.
72 QByteArray out = "[";
614
2/2
✓ Branch 17 → 9 taken 172 times.
✓ Branch 17 → 18 taken 72 times.
244 for (int i = 0; i < values.size(); ++i) {
615
2/2
✓ Branch 9 → 10 taken 100 times.
✓ Branch 9 → 11 taken 72 times.
172 if (i > 0)
616
1/1
✓ Branch 10 → 11 taken 100 times.
100 out.append(", ");
617
2/2
✓ Branch 12 → 13 taken 172 times.
✓ Branch 13 → 14 taken 172 times.
172 out.append(stringLiteral(values.at(i), lineEnd));
618 }
619
1/1
✓ Branch 18 → 19 taken 72 times.
72 out.append(']');
620 72 return out;
621 72 }
622
623 } // namespace Sieve
624