MailJDTourDownloadDocsQualityTestsCoverage

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

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.4% 661 / 0 / 700
Functions: 100.0% 41 / 0 / 41
Branches: 81.8% 894 / 0 / 1093

data/SieveRuleCodec.cpp
Line Branch Exec Source
1 #include "SieveRuleCodec.h"
2
3 #include "data/SieveMerge.h"
4
5 #include <QLoggingCategory>
6 #include <QRegularExpression>
7 #include <QSet>
8
9
4/7
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 8 taken 3307 times.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 13 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
3320 Q_LOGGING_CATEGORY(lcSieveCodec, "mailjd.sieve.codec")
10
11 namespace Sieve {
12
13 1693 bool Condition::operator==(const Condition &other) const {
14
3/4
✓ Branch 2 → 3 taken 1001 times.
✓ Branch 2 → 4 taken 692 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1001 times.
1693 if (kind != other.kind || negated != other.negated)
15 692 return false;
16
2/2
✓ Branch 5 → 6 taken 20 times.
✓ Branch 5 → 11 taken 981 times.
1001 if (kind == Kind::Size)
17
2/4
✓ Branch 6 → 7 taken 20 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 20 times.
✗ Branch 7 → 9 not taken.
20 return sizeOver == other.sizeOver && sizeLimit == other.sizeLimit;
18 1962 const auto lower = [](const QStringList &list) {
19 1962 QStringList result;
20
2/2
✓ Branch 10 → 4 taken 2026 times.
✓ Branch 10 → 11 taken 1962 times.
3988 for (const QString &value : list)
21
2/2
✓ Branch 5 → 6 taken 2026 times.
✓ Branch 6 → 7 taken 2026 times.
2026 result.append(value.toLower());
22 1962 return result;
23 ✗ };
24
6/12
✓ Branch 16 → 17 taken 876 times.
✓ Branch 16 → 24 taken 29 times.
✓ Branch 18 → 19 taken 857 times.
✓ Branch 18 → 24 taken 19 times.
✓ Branch 26 → 27 taken 981 times.
✗ Branch 26 → 28 not taken.
✓ Branch 28 → 29 taken 981 times.
✗ Branch 28 → 30 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
2762 return lower(headers) == lower(other.headers) && addressPart == other.addressPart &&
25
9/10
✓ Branch 11 → 12 taken 981 times.
✓ Branch 12 → 13 taken 981 times.
✓ Branch 13 → 14 taken 981 times.
✓ Branch 14 → 15 taken 905 times.
✓ Branch 14 → 24 taken 76 times.
✓ Branch 20 → 21 taken 857 times.
✗ Branch 20 → 24 not taken.
✓ Branch 21 → 22 taken 857 times.
✓ Branch 22 → 23 taken 660 times.
✓ Branch 22 → 24 taken 197 times.
2762 matchType == other.matchType && comparator == other.comparator && values == other.values;
26 }
27
28 707 bool Action::operator==(const Action &other) const {
29
2/4
✓ Branch 4 → 5 taken 707 times.
✗ Branch 4 → 12 not taken.
✓ Branch 6 → 7 taken 707 times.
✗ Branch 6 → 12 not taken.
707 return kind == other.kind && target == other.target && flags == other.flags &&
30
4/8
✓ Branch 2 → 3 taken 707 times.
✗ Branch 2 → 12 not taken.
✓ Branch 7 → 8 taken 707 times.
✗ Branch 7 → 12 not taken.
✓ Branch 8 → 9 taken 707 times.
✗ Branch 8 → 12 not taken.
✓ Branch 10 → 11 taken 707 times.
✗ Branch 10 → 12 not taken.
1414 copy == other.copy && create == other.create && specialUse == other.specialUse;
31 }
32
33 341 bool Rule::operator==(const Rule &other) const {
34
3/4
✓ Branch 4 → 5 taken 341 times.
✗ Branch 4 → 14 not taken.
✓ Branch 6 → 7 taken 34 times.
✓ Branch 6 → 8 taken 307 times.
1023 return name == other.name && enabled == other.enabled &&
35
1/2
✓ Branch 7 → 8 taken 34 times.
✗ Branch 7 → 14 not taken.
375 (conditions.size() < 2 || matchAll == other.matchAll) &&
36
4/8
✓ Branch 3 → 4 taken 341 times.
✗ Branch 3 → 14 not taken.
✓ Branch 9 → 10 taken 341 times.
✗ Branch 9 → 14 not taken.
✓ Branch 11 → 12 taken 341 times.
✗ Branch 11 → 14 not taken.
✓ Branch 12 → 13 taken 341 times.
✗ Branch 12 → 14 not taken.
682 conditions == other.conditions && actions == other.actions && stop == other.stop;
37 }
38
39 6 bool Vacation::operator==(const Vacation &other) const {
40
3/6
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 19 not taken.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 19 not taken.
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 19 not taken.
12 return enabled == other.enabled && firstDay == other.firstDay && lastDay == other.lastDay &&
41
2/4
✓ Branch 10 → 11 taken 6 times.
✗ Branch 10 → 19 not taken.
✓ Branch 12 → 13 taken 6 times.
✗ Branch 12 → 19 not taken.
12 zone == other.zone && subject == other.subject && message == other.message &&
42
4/8
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 19 not taken.
✓ Branch 13 → 14 taken 6 times.
✗ Branch 13 → 19 not taken.
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 19 not taken.
✓ Branch 17 → 18 taken 6 times.
✗ Branch 17 → 19 not taken.
12 days == other.days && addresses == other.addresses && from == other.from;
43 }
44
45 2546 QStringList Document::requiredExtensions() const {
46 // S106-085: a set keeps this linear; a server script may list ~10^5
47 // require entries within the 1 MiB script cap.
48 2546 QStringList result;
49 2546 QSet<QString> seen;
50
2/2
✓ Branch 18 → 5 taken 108598 times.
✓ Branch 18 → 19 taken 2546 times.
111144 for (const Block &block : blocks)
51
2/2
✓ Branch 15 → 8 taken 272197 times.
✓ Branch 15 → 16 taken 108598 times.
380795 for (const QString &extension : block.extensions)
52
1/2
✓ Branch 10 → 11 taken 272197 times.
✗ Branch 10 → 13 not taken.
272197 if (!seen.contains(extension)) {
53
1/1
✓ Branch 11 → 12 taken 272197 times.
272197 seen.insert(extension);
54
1/1
✓ Branch 12 → 13 taken 272197 times.
272197 result.append(extension);
55 }
56 2546 return result;
57 2546 }
58
59 138 int Document::ruleCount() const {
60 138 int count = 0;
61
2/2
✓ Branch 10 → 4 taken 4916 times.
✓ Branch 10 → 11 taken 138 times.
5054 for (const Block &block : blocks)
62
4/4
✓ Branch 5 → 6 taken 377 times.
✓ Branch 5 → 7 taken 4539 times.
✓ Branch 6 → 7 taken 110 times.
✓ Branch 6 → 8 taken 267 times.
4916 if (block.kind == BlockKind::Rule || block.kind == BlockKind::Advanced)
63 4649 ++count;
64 138 return count;
65 }
66
67 70 int Document::enabledRuleCount() const {
68 70 int count = 0;
69
2/2
✓ Branch 11 → 4 taken 686 times.
✓ Branch 11 → 12 taken 70 times.
756 for (const Block &block : blocks)
70
6/6
✓ Branch 5 → 6 taken 482 times.
✓ Branch 5 → 7 taken 204 times.
✓ Branch 6 → 7 taken 76 times.
✓ Branch 6 → 8 taken 406 times.
✓ Branch 7 → 8 taken 62 times.
✓ Branch 7 → 9 taken 218 times.
686 if ((block.kind == BlockKind::Rule && block.rule.enabled) || block.kind == BlockKind::Advanced)
71 468 ++count;
72 70 return count;
73 }
74
75 namespace {
76
77 // --- comment scanning -------------------------------------------------------
78
79 // Position after the "." line that ends the multi-line string whose body
80 // starts at `bodyStart`; -1 when it is unterminated. S106-093: a dot-stuffed
81 // body line ("..x") does not end the string.
82 59 int multiLineEnd(const QByteArray &bytes, int bodyStart) {
83 59 int lineStart = bodyStart;
84
1/2
✓ Branch 27 → 3 taken 179 times.
✗ Branch 27 → 28 not taken.
179 while (lineStart < bytes.size()) {
85 179 int lineEnd = bytes.indexOf('\n', lineStart);
86
3/3
✓ Branch 4 → 5 taken 178 times.
✓ Branch 4 → 6 taken 1 time.
✓ Branch 7 → 8 taken 179 times.
179 QByteArray line = bytes.mid(lineStart, lineEnd < 0 ? -1 : lineEnd - lineStart);
87
3/3
✓ Branch 8 → 9 taken 179 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 175 times.
179 if (line.endsWith('\r'))
88
1/1
✓ Branch 10 → 11 taken 4 times.
4 line.chop(1);
89
2/2
✓ Branch 12 → 13 taken 59 times.
✓ Branch 12 → 18 taken 120 times.
179 if (line == ".")
90
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 16 taken 58 times.
59 return lineEnd < 0 ? bytes.size() : lineEnd + 1;
91
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 120 times.
120 if (lineEnd < 0)
92 ✗ return -1;
93 120 lineStart = lineEnd + 1;
94 179 }
95 ✗ return -1;
96 }
97
98 // True when `bytes` contains a hash or bracket comment outside strings.
99 73765 bool hasComment(const QByteArray &bytes) {
100 73765 int pos = 0;
101
2/2
✓ Branch 70 → 3 taken 3691630 times.
✓ Branch 70 → 71 taken 73749 times.
3765379 while (pos < bytes.size()) {
102 3691630 const char c = bytes.at(pos);
103
2/2
✓ Branch 4 → 5 taken 240242 times.
✓ Branch 4 → 19 taken 3451388 times.
3691630 if (c == '"') {
104 240242 ++pos;
105
5/6
✓ Branch 12 → 13 taken 2553555 times.
✗ Branch 12 → 16 not taken.
✓ Branch 14 → 15 taken 2313313 times.
✓ Branch 14 → 16 taken 240242 times.
✓ Branch 17 → 6 taken 2313313 times.
✓ Branch 17 → 18 taken 240242 times.
2553555 while (pos < bytes.size() && bytes.at(pos) != '"')
106
2/2
✓ Branch 7 → 8 taken 5049 times.
✓ Branch 7 → 9 taken 2308264 times.
2313313 pos += bytes.at(pos) == '\\' ? 2 : 1;
107 240242 ++pos;
108
8/10
✓ Branch 19 → 20 taken 3451382 times.
✓ Branch 19 → 25 taken 6 times.
✓ Branch 20 → 21 taken 9 times.
✓ Branch 20 → 26 taken 3451373 times.
✓ Branch 22 → 23 taken 9 times.
✗ Branch 22 → 26 not taken.
✓ Branch 24 → 25 taken 9 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 15 times.
✓ Branch 27 → 29 taken 3451373 times.
3451388 } else if (c == '#' || (c == '/' && pos + 1 < bytes.size() && bytes.at(pos + 1) == '*')) {
109 15 return true;
110
13/18
✓ Branch 29 → 30 taken 3247732 times.
✓ Branch 29 → 31 taken 203641 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 36 taken 3247732 times.
✓ Branch 31 → 32 taken 203641 times.
✓ Branch 32 → 33 taken 203641 times.
✓ Branch 34 → 35 taken 60 times.
✓ Branch 34 → 36 taken 203581 times.
✓ Branch 37 → 38 taken 203641 times.
✓ Branch 37 → 39 taken 3247732 times.
✓ Branch 39 → 40 taken 203641 times.
✓ Branch 39 → 41 taken 3247732 times.
✓ Branch 41 → 42 taken 60 times.
✓ Branch 41 → 68 taken 3451313 times.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
✗ Branch 77 → 78 not taken.
✗ Branch 77 → 79 not taken.
3451373 } else if ((c == 't' || c == 'T') && bytes.mid(pos, 5).toLower() == "text:") {
111 // A hash comment may follow "text:" on its line (RFC 5228 §8.1).
112 60 int after = pos + 5;
113
6/8
✓ Branch 45 → 46 taken 61 times.
✗ Branch 45 → 51 not taken.
✓ Branch 47 → 48 taken 60 times.
✓ Branch 47 → 50 taken 1 time.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 60 times.
✓ Branch 52 → 43 taken 1 time.
✓ Branch 52 → 53 taken 60 times.
61 while (after < bytes.size() && (bytes.at(after) == ' ' || bytes.at(after) == '\t'))
114 1 ++after;
115
5/6
✓ Branch 54 → 55 taken 60 times.
✗ Branch 54 → 58 not taken.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 58 taken 59 times.
✓ Branch 59 → 60 taken 1 time.
✓ Branch 59 → 61 taken 59 times.
60 if (after < bytes.size() && bytes.at(after) == '#')
116 1 return true;
117 59 const int bodyStart = bytes.indexOf('\n', after);
118
1/2
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 59 times.
59 if (bodyStart < 0)
119 ✗ return false;
120 59 const int end = multiLineEnd(bytes, bodyStart + 1);
121
1/2
✗ Branch 65 → 66 not taken.
✓ Branch 65 → 67 taken 59 times.
59 if (end < 0)
122 ✗ return false;
123 59 pos = end;
124 } else {
125 3451313 ++pos;
126 }
127 }
128 73749 return false;
129 }
130
131 // --- test decoding ------------------------------------------------------------
132
133 76541 bool isMatchType(const QString &tag) {
134
4/4
✓ Branch 7 → 8 taken 439 times.
✓ Branch 7 → 14 taken 63866 times.
✓ Branch 10 → 11 taken 341 times.
✓ Branch 10 → 14 taken 98 times.
141285 return tag == QLatin1String(":is") || tag == QLatin1String(":contains") ||
135
4/4
✓ Branch 4 → 5 taken 64305 times.
✓ Branch 4 → 14 taken 12236 times.
✓ Branch 13 → 14 taken 65 times.
✓ Branch 13 → 15 taken 276 times.
141285 tag == QLatin1String(":matches") || tag == QLatin1String(":regex");
136 }
137
138 170 bool isAddressPart(const QString &tag) {
139
5/6
✓ Branch 4 → 5 taken 154 times.
✓ Branch 4 → 11 taken 16 times.
✓ Branch 7 → 8 taken 149 times.
✓ Branch 7 → 11 taken 5 times.
✓ Branch 10 → 11 taken 149 times.
✗ Branch 10 → 12 not taken.
319 return tag == QLatin1String(":all") || tag == QLatin1String(":localpart") ||
140 319 tag == QLatin1String(":domain");
141 }
142
143 76351 bool decodeComparisonArguments(const Test &test, Condition *condition,
144 QList<QStringList> *positional) {
145 76351 bool haveMatch = false;
146 76351 bool havePart = false;
147 76351 const QList<Argument> &args = test.arguments;
148
2/2
✓ Branch 67 → 3 taken 229098 times.
✓ Branch 67 → 68 taken 76265 times.
305363 for (int i = 0; i < args.size(); ++i) {
149 229098 const Argument &arg = args.at(i);
150
2/2
✓ Branch 4 → 5 taken 152495 times.
✓ Branch 4 → 9 taken 76603 times.
229098 if (arg.kind == Argument::Kind::StringList) {
151
2/2
✓ Branch 5 → 6 taken 152495 times.
✓ Branch 6 → 7 taken 152495 times.
152495 positional->append(arg.values());
152 152495 continue;
153 }
154
3/6
✓ Branch 9 → 10 taken 76603 times.
✗ Branch 9 → 12 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 76603 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 76603 times.
76603 if (arg.kind != Argument::Kind::Tag || !positional->isEmpty())
155 ✗ return false;
156
2/2
✓ Branch 18 → 19 taken 62 times.
✓ Branch 18 → 35 taken 76541 times.
76603 if (arg.text == QLatin1String(":comparator")) {
157
1/2
✓ Branch 22 → 23 taken 62 times.
✗ Branch 22 → 28 not taken.
124 if (!condition->comparator.isEmpty() || i + 1 >= args.size() ||
158
4/8
✓ Branch 20 → 21 taken 62 times.
✗ Branch 20 → 28 not taken.
✓ Branch 24 → 25 taken 62 times.
✗ Branch 24 → 28 not taken.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 62 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 62 times.
124 args.at(i + 1).kind != Argument::Kind::StringList || args.at(i + 1).strings.size() != 1)
159 ✗ return false;
160 62 condition->comparator = args.at(++i).strings.first().value;
161
5/6
✓ Branch 36 → 37 taken 76265 times.
✓ Branch 36 → 39 taken 276 times.
✓ Branch 37 → 38 taken 76265 times.
✗ Branch 37 → 39 not taken.
✓ Branch 40 → 41 taken 76265 times.
✓ Branch 40 → 45 taken 276 times.
76541 } else if (isMatchType(arg.text) && !haveMatch) {
162 76265 haveMatch = true;
163
1/1
✓ Branch 41 → 42 taken 76265 times.
76265 condition->matchType = arg.text.mid(1);
164
6/8
✓ Branch 45 → 46 taken 170 times.
✓ Branch 45 → 50 taken 106 times.
✓ Branch 47 → 48 taken 170 times.
✗ Branch 47 → 50 not taken.
✓ Branch 48 → 49 taken 170 times.
✗ Branch 48 → 50 not taken.
✓ Branch 51 → 52 taken 170 times.
✓ Branch 51 → 56 taken 106 times.
276 } else if (condition->kind == Condition::Kind::Address && isAddressPart(arg.text) && !havePart) {
165 170 havePart = true;
166
1/1
✓ Branch 52 → 53 taken 170 times.
170 condition->addressPart = arg.text.mid(1);
167
5/6
✓ Branch 56 → 57 taken 20 times.
✓ Branch 56 → 61 taken 86 times.
✓ Branch 59 → 60 taken 20 times.
✗ Branch 59 → 61 not taken.
✓ Branch 62 → 63 taken 20 times.
✓ Branch 62 → 64 taken 86 times.
106 } else if (condition->kind == Condition::Kind::Body && arg.text == QLatin1String(":text")) {
168 20 continue;
169 } else {
170 86 return false;
171 }
172 }
173 76265 return true;
174 }
175
176 76720 bool decodeCondition(const Test &input, Condition *condition) {
177 76720 const Test *test = &input;
178
2/2
✓ Branch 4 → 5 taken 125 times.
✓ Branch 4 → 19 taken 76595 times.
76720 if (test->name == QLatin1String("not")) {
179
4/8
✓ Branch 6 → 7 taken 125 times.
✗ Branch 6 → 10 not taken.
✓ Branch 7 → 8 taken 125 times.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 125 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 125 times.
125 if (!test->arguments.isEmpty() || test->testList || test->tests.size() != 1)
180 ✗ return false;
181 125 condition->negated = true;
182 125 test = &test->tests.first();
183
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 124 times.
125 if (test->name == QLatin1String("not"))
184 1 return false;
185 }
186
2/2
✓ Branch 20 → 21 taken 49 times.
✓ Branch 20 → 22 taken 76670 times.
76719 if (!test->tests.isEmpty())
187 49 return false;
188
2/2
✓ Branch 24 → 25 taken 242 times.
✓ Branch 24 → 53 taken 76428 times.
76670 if (test->name == QLatin1String("size")) {
189 242 condition->kind = Condition::Kind::Size;
190
3/6
✓ Branch 26 → 27 taken 242 times.
✗ Branch 26 → 31 not taken.
✓ Branch 28 → 29 taken 242 times.
✗ Branch 28 → 31 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 242 times.
484 if (test->arguments.size() != 2 || test->arguments.at(0).kind != Argument::Kind::Tag ||
191
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 242 times.
242 test->arguments.at(1).kind != Argument::Kind::Number)
192 ✗ return false;
193 242 const QString tag = test->arguments.at(0).text;
194
4/6
✓ Branch 39 → 40 taken 60 times.
✓ Branch 39 → 44 taken 182 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 60 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 242 times.
242 if (tag != QLatin1String(":over") && tag != QLatin1String(":under"))
195 ✗ return false;
196 242 condition->sizeOver = tag == QLatin1String(":over");
197 242 condition->sizeLimit = test->arguments.at(1).number;
198 242 return true;
199 242 }
200
2/2
✓ Branch 55 → 56 taken 13314 times.
✓ Branch 55 → 57 taken 63114 times.
76428 if (test->name == QLatin1String("address"))
201 13314 condition->kind = Condition::Kind::Address;
202
2/2
✓ Branch 59 → 60 taken 63002 times.
✓ Branch 59 → 61 taken 112 times.
63114 else if (test->name == QLatin1String("header"))
203 63002 condition->kind = Condition::Kind::Header;
204
2/2
✓ Branch 63 → 64 taken 35 times.
✓ Branch 63 → 65 taken 77 times.
112 else if (test->name == QLatin1String("body"))
205 35 condition->kind = Condition::Kind::Body;
206 else
207 77 return false;
208 76351 QList<QStringList> positional;
209
3/3
✓ Branch 66 → 67 taken 76351 times.
✓ Branch 67 → 68 taken 86 times.
✓ Branch 67 → 69 taken 76265 times.
76351 if (!decodeComparisonArguments(*test, condition, &positional))
210 86 return false;
211
2/2
✓ Branch 69 → 70 taken 35 times.
✓ Branch 69 → 77 taken 76230 times.
76265 if (condition->kind == Condition::Kind::Body) {
212
1/2
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 35 times.
35 if (positional.size() != 1)
213 ✗ return false;
214 35 condition->values = positional.at(0);
215 35 return !condition->values.isEmpty();
216 }
217
1/2
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 76230 times.
76230 if (positional.size() != 2)
218 ✗ return false;
219 76230 condition->headers = positional.at(0);
220 76230 condition->values = positional.at(1);
221
2/4
✓ Branch 85 → 86 taken 76230 times.
✗ Branch 85 → 89 not taken.
✓ Branch 87 → 88 taken 76230 times.
✗ Branch 87 → 89 not taken.
76230 return !condition->headers.isEmpty() && !condition->values.isEmpty();
222 76351 }
223
224 72366 bool decodeTest(const Test &test, Rule *rule) {
225
6/8
✓ Branch 4 → 5 taken 119 times.
✓ Branch 4 → 10 taken 72247 times.
✓ Branch 6 → 7 taken 119 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 119 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 119 times.
✓ Branch 11 → 13 taken 72247 times.
72366 if (test.name == QLatin1String("true") && test.arguments.isEmpty() && test.tests.isEmpty())
226 119 return true;
227
6/6
✓ Branch 15 → 16 taken 55002 times.
✓ Branch 15 → 19 taken 17245 times.
✓ Branch 18 → 19 taken 133 times.
✓ Branch 18 → 20 taken 54869 times.
✓ Branch 21 → 22 taken 17378 times.
✓ Branch 21 → 51 taken 54869 times.
72247 if (test.name == QLatin1String("allof") || test.name == QLatin1String("anyof")) {
228
4/8
✓ Branch 23 → 24 taken 17378 times.
✗ Branch 23 → 27 not taken.
✓ Branch 24 → 25 taken 17378 times.
✗ Branch 24 → 27 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 17378 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 17378 times.
17378 if (!test.arguments.isEmpty() || !test.testList || test.tests.isEmpty())
229 ✗ return false;
230 17378 rule->matchAll = test.name == QLatin1String("allof");
231 17378 rule->explicitList = test.tests.size() == 1;
232
2/2
✓ Branch 49 → 36 taken 21851 times.
✓ Branch 49 → 50 taken 17306 times.
39157 for (const Test &operand : test.tests) {
233
1/1
✓ Branch 37 → 38 taken 21851 times.
21851 Condition condition;
234
3/3
✓ Branch 38 → 39 taken 21851 times.
✓ Branch 39 → 40 taken 72 times.
✓ Branch 39 → 41 taken 21779 times.
21851 if (!decodeCondition(operand, &condition))
235 72 return false;
236
1/1
✓ Branch 41 → 42 taken 21779 times.
21779 rule->conditions.append(condition);
237 21851 }
238 17306 return true;
239 }
240
1/1
✓ Branch 51 → 52 taken 54869 times.
54869 Condition condition;
241
3/3
✓ Branch 52 → 53 taken 54869 times.
✓ Branch 53 → 54 taken 141 times.
✓ Branch 53 → 55 taken 54728 times.
54869 if (!decodeCondition(test, &condition))
242 141 return false;
243
1/1
✓ Branch 55 → 56 taken 54728 times.
54728 rule->conditions.append(condition);
244 54728 return true;
245 54869 }
246
247 // --- action decoding ----------------------------------------------------------
248
249 72784 bool singleString(const Argument &arg, QString *value) {
250
3/6
✓ Branch 2 → 3 taken 72784 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 72784 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 72784 times.
72784 if (arg.kind != Argument::Kind::StringList || arg.strings.size() != 1)
251 ✗ return false;
252 72784 *value = arg.strings.first().value;
253 72784 return true;
254 }
255
256 70726 bool decodeFileInto(const Command &command, Action *action) {
257 70726 action->kind = Action::Kind::FileInto;
258 70726 const QList<Argument> &args = command.arguments;
259
1/2
✓ Branch 44 → 3 taken 70829 times.
✗ Branch 44 → 45 not taken.
70829 for (int i = 0; i < args.size(); ++i) {
260 70829 const Argument &arg = args.at(i);
261
2/2
✓ Branch 4 → 5 taken 70726 times.
✓ Branch 4 → 9 taken 103 times.
70829 if (arg.kind == Argument::Kind::StringList) {
262
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 70726 times.
70726 if (i != args.size() - 1)
263 ✗ return false;
264 70726 return singleString(arg, &action->target);
265 }
266
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 103 times.
103 if (arg.kind != Argument::Kind::Tag)
267 ✗ return false;
268
5/6
✓ Branch 13 → 14 taken 53 times.
✓ Branch 13 → 16 taken 50 times.
✓ Branch 14 → 15 taken 53 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 53 times.
✓ Branch 17 → 19 taken 50 times.
103 if (arg.text == QLatin1String(":copy") && !action->copy)
269 53 action->copy = true;
270
5/6
✓ Branch 21 → 22 taken 30 times.
✓ Branch 21 → 24 taken 20 times.
✓ Branch 22 → 23 taken 30 times.
✗ Branch 22 → 24 not taken.
✓ Branch 25 → 26 taken 30 times.
✓ Branch 25 → 27 taken 20 times.
50 else if (arg.text == QLatin1String(":create") && !action->create)
271 30 action->create = true;
272
2/4
✓ Branch 31 → 32 taken 20 times.
✗ Branch 31 → 38 not taken.
✓ Branch 33 → 34 taken 20 times.
✗ Branch 33 → 38 not taken.
60 else if (arg.text == QLatin1String(":specialuse") && action->specialUse.isEmpty() &&
273
3/6
✓ Branch 29 → 30 taken 20 times.
✗ Branch 29 → 38 not taken.
✓ Branch 36 → 37 taken 20 times.
✗ Branch 36 → 38 not taken.
✓ Branch 39 → 40 taken 20 times.
✗ Branch 39 → 41 not taken.
60 i + 1 < args.size() && singleString(args.at(i + 1), &action->specialUse))
274 20 ++i;
275 else
276 ✗ return false;
277 }
278 ✗ return false;
279 }
280
281 77311 bool decodeAction(const Command &command, Action *action) {
282
5/6
✓ Branch 2 → 3 taken 77304 times.
✓ Branch 2 → 5 taken 7 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 77304 times.
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 9 taken 77304 times.
77311 if (command.hasBlock || !command.tests.isEmpty())
283 7 return false;
284 77304 const QString &name = command.name;
285 77304 const QList<Argument> &args = command.arguments;
286
2/2
✓ Branch 11 → 12 taken 70726 times.
✓ Branch 11 → 13 taken 6578 times.
77304 if (name == QLatin1String("fileinto"))
287 70726 return decodeFileInto(command, action);
288
2/2
✓ Branch 15 → 16 taken 489 times.
✓ Branch 15 → 37 taken 6089 times.
6578 if (name == QLatin1String("redirect")) {
289 489 action->kind = Action::Kind::Redirect;
290
4/6
✓ Branch 17 → 18 taken 486 times.
✓ Branch 17 → 25 taken 3 times.
✓ Branch 19 → 20 taken 486 times.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 486 times.
✗ Branch 23 → 25 not taken.
975 if (args.size() == 2 && args.at(0).kind == Argument::Kind::Tag &&
291
2/2
✓ Branch 26 → 27 taken 486 times.
✓ Branch 26 → 29 taken 3 times.
975 args.at(0).text == QLatin1String(":copy")) {
292 486 action->copy = true;
293 486 return singleString(args.at(1), &action->target);
294 }
295
2/4
✓ Branch 30 → 31 taken 3 times.
✗ Branch 30 → 35 not taken.
✓ Branch 33 → 34 taken 3 times.
✗ Branch 33 → 35 not taken.
3 return args.size() == 1 && singleString(args.at(0), &action->target);
296 }
297
6/6
✓ Branch 39 → 40 taken 3544 times.
✓ Branch 39 → 46 taken 2545 times.
✓ Branch 42 → 43 taken 1721 times.
✓ Branch 42 → 46 taken 1823 times.
✓ Branch 45 → 46 taken 476 times.
✓ Branch 45 → 47 taken 1245 times.
7810 if (name == QLatin1String("addflag") || name == QLatin1String("setflag") ||
298
2/2
✓ Branch 48 → 49 taken 4844 times.
✓ Branch 48 → 72 taken 1245 times.
7810 name == QLatin1String("removeflag")) {
299
2/2
✓ Branch 51 → 52 taken 2545 times.
✓ Branch 51 → 53 taken 2299 times.
7143 action->kind = name == QLatin1String("addflag") ? Action::Kind::AddFlag
300
2/2
✓ Branch 55 → 56 taken 1823 times.
✓ Branch 55 → 57 taken 476 times.
2299 : name == QLatin1String("setflag") ? Action::Kind::SetFlag
301 : Action::Kind::RemoveFlag;
302
5/6
✓ Branch 60 → 61 taken 4843 times.
✓ Branch 60 → 63 taken 1 time.
✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 4843 times.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 67 taken 4843 times.
4844 if (args.size() != 1 || args.at(0).kind != Argument::Kind::StringList)
303 1 return false;
304
1/1
✓ Branch 68 → 69 taken 4843 times.
4843 action->flags = args.at(0).values();
305 4843 return true;
306 }
307
6/6
✓ Branch 74 → 75 taken 1003 times.
✓ Branch 74 → 78 taken 242 times.
✓ Branch 77 → 78 taken 61 times.
✓ Branch 77 → 79 taken 942 times.
✓ Branch 80 → 81 taken 303 times.
✓ Branch 80 → 87 taken 942 times.
1245 if (name == QLatin1String("keep") || name == QLatin1String("discard")) {
308
2/2
✓ Branch 83 → 84 taken 242 times.
✓ Branch 83 → 85 taken 61 times.
303 action->kind = name == QLatin1String("keep") ? Action::Kind::Keep : Action::Kind::Discard;
309 303 return args.isEmpty();
310 }
311
2/2
✓ Branch 89 → 90 taken 929 times.
✓ Branch 89 → 98 taken 13 times.
942 if (name == QLatin1String("reject")) {
312 929 action->kind = Action::Kind::Reject;
313
2/4
✓ Branch 91 → 92 taken 929 times.
✗ Branch 91 → 96 not taken.
✓ Branch 94 → 95 taken 929 times.
✗ Branch 94 → 96 not taken.
929 return args.size() == 1 && singleString(args.at(0), &action->target);
314 }
315 13 return false;
316 }
317
318 72153 bool decodeActions(const QList<Command> &commands, Rule *rule) {
319
2/2
✓ Branch 31 → 3 taken 141233 times.
✓ Branch 31 → 32 taken 72131 times.
213364 for (int i = 0; i < commands.size(); ++i) {
320 141233 const Command &command = commands.at(i);
321
2/2
✓ Branch 6 → 7 taken 63922 times.
✓ Branch 6 → 19 taken 77311 times.
141233 if (command.name == QLatin1String("stop")) {
322
6/8
✓ Branch 8 → 9 taken 63921 times.
✓ Branch 8 → 14 taken 1 time.
✓ Branch 10 → 11 taken 63921 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 63921 times.
✗ Branch 12 → 14 not taken.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 63921 times.
127843 if (i != commands.size() - 1 || !command.arguments.isEmpty() || !command.tests.isEmpty() ||
323
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 63921 times.
63921 command.hasBlock)
324 22 return false;
325 63921 rule->stop = true;
326 63921 continue;
327 }
328 77311 Action action;
329
3/3
✓ Branch 19 → 20 taken 77311 times.
✓ Branch 20 → 21 taken 21 times.
✓ Branch 20 → 22 taken 77290 times.
77311 if (!decodeAction(command, &action))
330 21 return false;
331
1/1
✓ Branch 22 → 23 taken 77290 times.
77290 rule->actions.append(action);
332 77311 }
333
3/4
✓ Branch 33 → 34 taken 16 times.
✓ Branch 33 → 35 taken 72115 times.
✓ Branch 34 → 35 taken 16 times.
✗ Branch 34 → 36 not taken.
72131 return !rule->actions.isEmpty() || rule->stop;
334 }
335
336 // --- vacation decoding --------------------------------------------------------
337
338 71 bool decodeDateTest(const Test &test, Vacation *vacation) {
339
3/6
✓ Branch 4 → 5 taken 71 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 71 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 71 times.
71 if (test.name != QLatin1String("currentdate") || !test.tests.isEmpty())
340 ✗ return false;
341 71 QString zone;
342 71 QString relation;
343 71 QStringList positional;
344 71 const QList<Argument> &args = test.arguments;
345
2/2
✓ Branch 50 → 12 taken 264 times.
✓ Branch 50 → 51 taken 71 times.
335 for (int i = 0; i < args.size(); ++i) {
346 264 const Argument &arg = args.at(i);
347
2/2
✓ Branch 13 → 14 taken 142 times.
✓ Branch 13 → 22 taken 122 times.
264 if (arg.kind == Argument::Kind::StringList) {
348
3/3
✓ Branch 14 → 15 taken 142 times.
✓ Branch 17 → 18 taken 142 times.
✓ Branch 18 → 19 taken 142 times.
142 positional.append(arg.values().join(QLatin1Char('\x1f')));
349 142 continue;
350 }
351
3/6
✓ Branch 22 → 23 taken 122 times.
✗ Branch 22 → 25 not taken.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 122 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 122 times.
122 if (arg.kind != Argument::Kind::Tag || i + 1 >= args.size())
352 ✗ return false;
353 122 QString value;
354
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 122 times.
122 if (!singleString(args.at(i + 1), &value))
355 ✗ return false;
356
2/2
✓ Branch 35 → 36 taken 51 times.
✓ Branch 35 → 37 taken 71 times.
122 if (arg.text == QLatin1String(":zone"))
357 51 zone = value;
358
1/2
✓ Branch 39 → 40 taken 71 times.
✗ Branch 39 → 41 not taken.
71 else if (arg.text == QLatin1String(":value"))
359 71 relation = value;
360 else
361 ✗ return false;
362 122 ++i;
363 122 }
364
5/6
✓ Branch 52 → 53 taken 71 times.
✗ Branch 52 → 57 not taken.
✓ Branch 56 → 57 taken 6 times.
✓ Branch 56 → 58 taken 65 times.
✓ Branch 59 → 60 taken 6 times.
✓ Branch 59 → 61 taken 65 times.
71 if (positional.size() != 2 || positional.at(0) != QLatin1String("date"))
365 6 return false;
366
1/1
✓ Branch 62 → 63 taken 65 times.
65 const QDate day = QDate::fromString(positional.at(1), Qt::ISODate);
367
6/9
✓ Branch 63 → 64 taken 65 times.
✓ Branch 64 → 65 taken 65 times.
✗ Branch 64 → 71 not taken.
✓ Branch 67 → 68 taken 37 times.
✓ Branch 67 → 72 taken 28 times.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 37 times.
✗ Branch 73 → 74 not taken.
✓ Branch 73 → 75 taken 65 times.
65 if (!day.isValid() || (relation != QLatin1String("ge") && relation != QLatin1String("le")))
368 ✗ return false;
369
4/6
✓ Branch 76 → 77 taken 21 times.
✓ Branch 76 → 80 taken 44 times.
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 21 times.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 83 taken 65 times.
65 if (!vacation->zone.isEmpty() && vacation->zone != zone)
370 ✗ return false;
371 65 vacation->zone = zone;
372
2/2
✓ Branch 86 → 87 taken 28 times.
✓ Branch 86 → 88 taken 37 times.
65 QDate &target = relation == QLatin1String("ge") ? vacation->firstDay : vacation->lastDay;
373
2/3
✓ Branch 89 → 90 taken 65 times.
✗ Branch 90 → 91 not taken.
✓ Branch 90 → 92 taken 65 times.
65 if (target.isValid())
374 ✗ return false;
375 65 target = day;
376 65 return true;
377 71 }
378
379 247 bool decodeVacationCommand(const Command &command, Vacation *vacation) {
380
4/8
✓ Branch 4 → 5 taken 247 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 247 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 247 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 247 times.
247 if (command.name != QLatin1String("vacation") || command.hasBlock || !command.tests.isEmpty())
381 ✗ return false;
382 247 const QList<Argument> &args = command.arguments;
383
3/6
✓ Branch 13 → 14 taken 247 times.
✗ Branch 13 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 247 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 247 times.
247 if (args.isEmpty() || !singleString(args.last(), &vacation->message))
384 ✗ return false;
385 // S106-094: re-rendering must keep the meaning — no ":days" stays absent
386 // (days 0; the server default, RFC 5230 recommends 7), and a value the
387 // model cannot hold (0, above 365) or a repeated tag keeps the block
388 // advanced instead of being clamped or decided last-wins.
389 247 vacation->days = 0;
390 247 QSet<QString> seenTags;
391
2/2
✓ Branch 76 → 23 taken 624 times.
✓ Branch 76 → 77 taken 238 times.
862 for (int i = 0; i < args.size() - 1; ++i) {
392 624 const Argument &arg = args.at(i);
393
3/6
✓ Branch 24 → 25 taken 624 times.
✗ Branch 24 → 27 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 624 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 624 times.
624 if (arg.kind != Argument::Kind::Tag || i + 1 >= args.size() - 1)
394 ✗ return false;
395
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 623 times.
624 if (seenTags.contains(arg.text))
396 1 return false;
397
1/1
✓ Branch 34 → 35 taken 623 times.
623 seenTags.insert(arg.text);
398 623 const Argument &value = args.at(i + 1);
399
1/2
✓ Branch 39 → 40 taken 239 times.
✗ Branch 39 → 43 not taken.
862 if (arg.text == QLatin1String(":days") && value.kind == Argument::Kind::Number &&
400
8/8
✓ Branch 38 → 39 taken 239 times.
✓ Branch 38 → 43 taken 384 times.
✓ Branch 40 → 41 taken 238 times.
✓ Branch 40 → 43 taken 1 time.
✓ Branch 41 → 42 taken 237 times.
✓ Branch 41 → 43 taken 1 time.
✓ Branch 44 → 45 taken 237 times.
✓ Branch 44 → 46 taken 386 times.
862 value.number >= 1 && value.number <= 365)
401 237 vacation->days = static_cast<int>(value.number);
402
5/6
✓ Branch 48 → 49 taken 230 times.
✓ Branch 48 → 52 taken 156 times.
✓ Branch 50 → 51 taken 230 times.
✗ Branch 50 → 52 not taken.
✓ Branch 53 → 54 taken 156 times.
✓ Branch 53 → 74 taken 230 times.
386 else if (arg.text == QLatin1String(":subject") && singleString(value, &vacation->subject))
403 ;
404
5/6
✓ Branch 56 → 57 taken 21 times.
✓ Branch 56 → 60 taken 135 times.
✓ Branch 58 → 59 taken 21 times.
✗ Branch 58 → 60 not taken.
✓ Branch 61 → 62 taken 135 times.
✓ Branch 61 → 74 taken 21 times.
156 else if (arg.text == QLatin1String(":from") && singleString(value, &vacation->from))
405 ;
406
5/6
✓ Branch 64 → 65 taken 127 times.
✓ Branch 64 → 67 taken 8 times.
✓ Branch 65 → 66 taken 127 times.
✗ Branch 65 → 67 not taken.
✓ Branch 68 → 69 taken 127 times.
✓ Branch 68 → 73 taken 8 times.
135 else if (arg.text == QLatin1String(":addresses") && value.kind == Argument::Kind::StringList)
407
1/1
✓ Branch 69 → 70 taken 127 times.
127 vacation->addresses = value.values();
408 else
409 8 return false;
410 615 ++i;
411 }
412 238 return true;
413 247 }
414
415 215 bool decodeVacationTest(const Test &test, Vacation *vacation) {
416
6/8
✓ Branch 4 → 5 taken 172 times.
✓ Branch 4 → 10 taken 43 times.
✓ Branch 6 → 7 taken 172 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 172 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 172 times.
✓ Branch 11 → 13 taken 43 times.
215 if (test.name == QLatin1String("true") && test.arguments.isEmpty() && test.tests.isEmpty())
417 172 return true;
418
2/2
✓ Branch 15 → 16 taken 28 times.
✓ Branch 15 → 36 taken 15 times.
43 if (test.name == QLatin1String("allof")) {
419
4/8
✓ Branch 17 → 18 taken 28 times.
✗ Branch 17 → 22 not taken.
✓ Branch 19 → 20 taken 28 times.
✗ Branch 19 → 22 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 28 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 28 times.
28 if (!test.arguments.isEmpty() || test.tests.isEmpty() || test.tests.size() > 2)
420 ✗ return false;
421
2/2
✓ Branch 34 → 28 taken 56 times.
✓ Branch 34 → 35 taken 28 times.
84 for (const Test &operand : test.tests)
422
2/3
✓ Branch 29 → 30 taken 56 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 56 times.
56 if (!decodeDateTest(operand, vacation))
423 ✗ return false;
424 28 return true;
425 }
426 15 return decodeDateTest(test, vacation);
427 }
428
429 // --- block segmentation -------------------------------------------------------
430
431 struct Group {
432 int first = 0; // command index
433 int last = 0;
434 };
435
436 // The `if` test hidden in Roundcube's disabled form "if false # <test>".
437 // A test spanning lines continues on "# " lines (renderIfHeader()). The
438 // form is only recognised when re-rendering it loses nothing: no comment
439 // before "false", only the hidden test's comment lines (and blank lines
440 // after them) up to "{", and no comment inside the hidden test (S106-095).
441 72594 bool disabledTest(const QByteArray &source, const Command &command, Test *test) {
442 72594 const Test &visible = command.tests.first();
443
5/6
✓ Branch 5 → 6 taken 568 times.
✓ Branch 5 → 10 taken 72026 times.
✓ Branch 7 → 8 taken 568 times.
✗ Branch 7 → 10 not taken.
✓ Branch 12 → 13 taken 72026 times.
✓ Branch 12 → 14 taken 568 times.
73162 if (visible.name != QLatin1String("false") || !visible.arguments.isEmpty() ||
444
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 568 times.
568 !visible.tests.isEmpty())
445 72026 return false;
446
4/4
✓ Branch 14 → 15 taken 568 times.
✓ Branch 15 → 16 taken 568 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 567 times.
568 if (hasComment(source.mid(command.span.begin, visible.span.end - command.span.begin)))
447 1 return false;
448 const QByteArray between =
449
1/1
✓ Branch 19 → 20 taken 567 times.
567 source.mid(visible.span.end, command.blockSpan.begin - visible.span.end);
450
1/1
✓ Branch 20 → 21 taken 567 times.
567 const QList<QByteArray> lines = between.split('\n');
451
1/1
✓ Branch 22 → 23 taken 567 times.
567 const QByteArray first = lines.first().trimmed();
452
3/3
✓ Branch 23 → 24 taken 567 times.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 566 times.
567 if (!first.startsWith('#'))
453 1 return false;
454
2/2
✓ Branch 26 → 27 taken 566 times.
✓ Branch 27 → 28 taken 566 times.
566 QByteArray comment = first.mid(1).trimmed();
455 // A CRLF script renders its continuation lines with CRLF: the '\r' ends
456 // the comment line, it is not part of the hidden test (a multi-line
457 // string's "." terminator would read ".\r"; fuzz_sieve_script).
458
4/4
✓ Branch 30 → 31 taken 566 times.
✓ Branch 31 → 32 taken 4 times.
✓ Branch 31 → 33 taken 562 times.
✓ Branch 34 → 35 taken 566 times.
566 QByteArray lineBreak = lines.first().endsWith('\r') ? "\r\n" : "\n";
459 566 bool blankSeen = false;
460
2/2
✓ Branch 67 → 36 taken 570 times.
✓ Branch 67 → 68 taken 565 times.
1135 for (int i = 1; i < lines.size(); ++i) {
461 570 const QByteArray &line = lines.at(i);
462
3/3
✓ Branch 37 → 38 taken 570 times.
✓ Branch 40 → 41 taken 566 times.
✓ Branch 40 → 42 taken 4 times.
570 if (line.trimmed().isEmpty()) {
463 566 blankSeen = true;
464 566 continue;
465 }
466
6/7
✓ Branch 42 → 43 taken 4 times.
✗ Branch 42 → 45 not taken.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 3 times.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 3 times.
4 if (blankSeen || !line.startsWith('#'))
467 1 return false;
468
1/1
✓ Branch 49 → 50 taken 3 times.
3 QByteArray continuation = line.mid(1);
469
2/3
✓ Branch 50 → 51 taken 3 times.
✓ Branch 51 → 52 taken 3 times.
✗ Branch 51 → 53 not taken.
3 if (continuation.startsWith(' '))
470
1/1
✓ Branch 52 → 53 taken 3 times.
3 continuation.remove(0, 1);
471
1/1
✓ Branch 53 → 54 taken 3 times.
3 const bool crlf = continuation.endsWith('\r');
472
1/2
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 3 times.
3 if (crlf)
473 ✗ continuation.chop(1);
474
2/2
✓ Branch 56 → 57 taken 3 times.
✓ Branch 57 → 58 taken 3 times.
3 comment += lineBreak + continuation;
475
2/3
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 3 times.
✓ Branch 62 → 63 taken 3 times.
3 lineBreak = crlf ? "\r\n" : "\n";
476 3 }
477
3/3
✓ Branch 68 → 69 taken 565 times.
✓ Branch 69 → 70 taken 2 times.
✓ Branch 69 → 71 taken 563 times.
565 if (hasComment(comment))
478 2 return false;
479
1/1
✓ Branch 71 → 72 taken 563 times.
563 return parseTestExpression(comment, test);
480 567 }
481
482 92729 bool decodeIfBlock(const QByteArray &source, const Command &command, Block *block) {
483
2/4
✓ Branch 6 → 7 taken 72594 times.
✗ Branch 6 → 11 not taken.
✓ Branch 8 → 9 taken 72594 times.
✗ Branch 8 → 11 not taken.
237917 if (command.name != QLatin1String("if") || !command.arguments.isEmpty() ||
484
6/8
✓ Branch 4 → 5 taken 72594 times.
✓ Branch 4 → 11 taken 20135 times.
✓ Branch 9 → 10 taken 72594 times.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 72594 times.
✓ Branch 13 → 14 taken 20135 times.
✓ Branch 13 → 15 taken 72594 times.
237917 command.tests.size() != 1 || command.testList || !command.hasBlock)
485 20135 return false;
486 72594 Test test = command.tests.first();
487 72594 bool enabled = true;
488 72594 Test hidden;
489
3/3
✓ Branch 17 → 18 taken 72594 times.
✓ Branch 18 → 19 taken 563 times.
✓ Branch 18 → 20 taken 72031 times.
72594 if (disabledTest(source, command, &hidden)) {
490 563 enabled = false;
491 563 test = hidden;
492
4/4
✓ Branch 21 → 22 taken 72031 times.
✓ Branch 22 → 23 taken 72031 times.
✓ Branch 24 → 25 taken 13 times.
✓ Branch 24 → 26 taken 72018 times.
72031 } else if (hasComment(source.mid(command.span.begin, command.span.length()))) {
493 // Comments are never re-rendered, so a commented rule stays advanced.
494 13 return false;
495 }
496
8/12
✓ Branch 26 → 27 taken 563 times.
✓ Branch 26 → 32 taken 72018 times.
✓ Branch 28 → 29 taken 563 times.
✓ Branch 29 → 30 taken 563 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 563 times.
✓ Branch 33 → 34 taken 563 times.
✓ Branch 33 → 35 taken 72018 times.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 72581 times.
✗ Branch 81 → 82 not taken.
✗ Branch 81 → 83 not taken.
72581 if (!enabled && hasComment(source.mid(command.blockSpan.begin, command.blockSpan.length())))
497 ✗ return false;
498
6/6
✓ Branch 38 → 39 taken 8444 times.
✓ Branch 38 → 44 taken 64137 times.
✓ Branch 42 → 43 taken 215 times.
✓ Branch 42 → 44 taken 8229 times.
✓ Branch 45 → 46 taken 215 times.
✓ Branch 45 → 61 taken 72366 times.
72581 if (command.block.size() == 1 && command.block.first().name == QLatin1String("vacation")) {
499 215 Vacation vacation;
500
1/1
✓ Branch 46 → 47 taken 215 times.
215 if (decodeVacationTest(test, &vacation) &&
501
8/10
✓ Branch 47 → 48 taken 209 times.
✓ Branch 47 → 54 taken 6 times.
✓ Branch 49 → 50 taken 209 times.
✓ Branch 50 → 51 taken 209 times.
✗ Branch 50 → 54 not taken.
✓ Branch 51 → 52 taken 209 times.
✓ Branch 52 → 53 taken 209 times.
✗ Branch 52 → 54 not taken.
✓ Branch 55 → 56 taken 209 times.
✓ Branch 55 → 58 taken 6 times.
215 decodeVacationCommand(command.block.first(), &vacation) && representable(vacation)) {
502 209 vacation.enabled = enabled;
503 209 block->kind = BlockKind::Vacation;
504 209 block->vacation = vacation;
505 209 return true;
506 }
507 6 return false;
508 215 }
509 72366 Rule rule;
510
11/11
✓ Branch 61 → 62 taken 72366 times.
✓ Branch 62 → 63 taken 72153 times.
✓ Branch 62 → 67 taken 213 times.
✓ Branch 63 → 64 taken 72153 times.
✓ Branch 64 → 65 taken 72131 times.
✓ Branch 64 → 67 taken 22 times.
✓ Branch 65 → 66 taken 72131 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 66 → 68 taken 72129 times.
✓ Branch 69 → 70 taken 237 times.
✓ Branch 69 → 71 taken 72129 times.
72366 if (!decodeTest(test, &rule) || !decodeActions(command.block, &rule) || !representable(rule))
511 237 return false;
512 72129 rule.enabled = enabled;
513 72129 block->kind = BlockKind::Rule;
514 72129 block->rule = rule;
515 72129 return true;
516 72594 }
517
518 3263 QList<Group> groupCommands(const QList<Command> &commands) {
519 3263 QList<Group> groups;
520
2/2
✓ Branch 24 → 3 taken 95127 times.
✓ Branch 24 → 25 taken 3263 times.
98390 for (int i = 0; i < commands.size(); ++i) {
521 95127 const QString &name = commands.at(i).name;
522
4/4
✓ Branch 6 → 7 taken 95104 times.
✓ Branch 6 → 10 taken 23 times.
✓ Branch 9 → 10 taken 23 times.
✓ Branch 9 → 11 taken 95081 times.
95127 const bool continues = name == QLatin1String("elsif") || name == QLatin1String("else");
523
5/6
✓ Branch 12 → 13 taken 46 times.
✓ Branch 12 → 16 taken 95081 times.
✓ Branch 14 → 15 taken 46 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 46 times.
✓ Branch 17 → 20 taken 95081 times.
95127 if (continues && !groups.isEmpty())
524
1/1
✓ Branch 18 → 19 taken 46 times.
46 groups.last().last = i;
525 else
526
1/1
✓ Branch 20 → 21 taken 95081 times.
95081 groups.append({i, i});
527 }
528 3263 return groups;
529 ✗ }
530
531 // End of a block: after the command, trailing blanks, an optional same-line
532 // comment and one line end.
533 95081 int blockEnd(const QByteArray &source, int commandEnd) {
534 95081 int pos = commandEnd;
535
7/8
✓ Branch 5 → 6 taken 95057 times.
✓ Branch 5 → 11 taken 38 times.
✓ Branch 7 → 8 taken 95043 times.
✓ Branch 7 → 10 taken 14 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 95043 times.
✓ Branch 12 → 3 taken 14 times.
✓ Branch 12 → 13 taken 95081 times.
95095 while (pos < source.size() && (source.at(pos) == ' ' || source.at(pos) == '\t'))
536 14 ++pos;
537
6/6
✓ Branch 14 → 15 taken 95043 times.
✓ Branch 14 → 18 taken 38 times.
✓ Branch 16 → 17 taken 8 times.
✓ Branch 16 → 18 taken 95035 times.
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 26 taken 95073 times.
95081 if (pos < source.size() && source.at(pos) == '#') {
538 8 const int lineEnd = source.indexOf('\n', pos);
539
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 8 times.
8 return lineEnd < 0 ? source.size() : lineEnd + 1;
540 }
541
8/10
✓ Branch 27 → 28 taken 95035 times.
✓ Branch 27 → 35 taken 38 times.
✓ Branch 29 → 30 taken 54 times.
✓ Branch 29 → 35 taken 94981 times.
✓ Branch 31 → 32 taken 54 times.
✗ Branch 31 → 35 not taken.
✓ Branch 33 → 34 taken 54 times.
✗ Branch 33 → 35 not taken.
✓ Branch 36 → 37 taken 54 times.
✓ Branch 36 → 38 taken 95019 times.
95127 if (pos < source.size() && source.at(pos) == '\r' && pos + 1 < source.size() &&
542 54 source.at(pos + 1) == '\n')
543 54 return pos + 2;
544
6/6
✓ Branch 39 → 40 taken 94981 times.
✓ Branch 39 → 43 taken 38 times.
✓ Branch 41 → 42 taken 94975 times.
✓ Branch 41 → 43 taken 6 times.
✓ Branch 44 → 45 taken 94975 times.
✓ Branch 44 → 46 taken 44 times.
95019 if (pos < source.size() && source.at(pos) == '\n')
545 94975 return pos + 1;
546 44 return commandEnd;
547 }
548
549 // Splits the leading bytes into (kept prefix, marker name). The marker must
550 // be the last non-blank line before the command.
551 95081 bool splitMarker(const QByteArray &leading, QByteArray *prefix, QString *name) {
552
4/7
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 15 taken 95068 times.
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 15 not taken.
✓ Branch 9 → 10 taken 13 times.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
95094 static const QRegularExpression marker(QStringLiteral("^#\\s*rule:\\[(.*)\\]\\s*$"));
553 95081 QByteArray trimmed = leading;
554
9/10
✓ Branch 19 → 20 taken 72777 times.
✓ Branch 19 → 25 taken 22314 times.
✓ Branch 20 → 21 taken 72777 times.
✓ Branch 21 → 22 taken 72767 times.
✓ Branch 21 → 24 taken 10 times.
✓ Branch 22 → 23 taken 72767 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 72767 times.
✓ Branch 26 → 17 taken 10 times.
✓ Branch 26 → 27 taken 95081 times.
95091 while (!trimmed.isEmpty() && (trimmed.endsWith(' ') || trimmed.endsWith('\t')))
555
1/1
✓ Branch 17 → 18 taken 10 times.
10 trimmed.chop(1);
556
3/3
✓ Branch 27 → 28 taken 95081 times.
✓ Branch 28 → 29 taken 72767 times.
✓ Branch 28 → 30 taken 22314 times.
95081 if (trimmed.endsWith('\n'))
557
1/1
✓ Branch 29 → 30 taken 72767 times.
72767 trimmed.chop(1);
558
3/3
✓ Branch 30 → 31 taken 95081 times.
✓ Branch 31 → 32 taken 28 times.
✓ Branch 31 → 33 taken 95053 times.
95081 if (trimmed.endsWith('\r'))
559
1/1
✓ Branch 32 → 33 taken 28 times.
28 trimmed.chop(1);
560 95081 const int lineStart = trimmed.lastIndexOf('\n') + 1;
561
2/2
✓ Branch 34 → 35 taken 95081 times.
✓ Branch 35 → 36 taken 95081 times.
95081 const QString line = QString::fromUtf8(trimmed.mid(lineStart));
562
1/1
✓ Branch 38 → 39 taken 95081 times.
95081 const QRegularExpressionMatch match = marker.match(line);
563
3/3
✓ Branch 39 → 40 taken 95081 times.
✓ Branch 40 → 41 taken 53169 times.
✓ Branch 40 → 42 taken 41912 times.
95081 if (!match.hasMatch())
564 53169 return false;
565
1/1
✓ Branch 42 → 43 taken 41912 times.
41912 *prefix = leading.left(lineStart);
566
1/1
✓ Branch 45 → 46 taken 41912 times.
41912 *name = match.captured(1);
567 41912 return true;
568 95081 }
569
570 95053 bool isRequireCommand(const Command &command, QStringList *extensions) {
571
3/6
✓ Branch 5 → 6 taken 2324 times.
✗ Branch 5 → 12 not taken.
✓ Branch 7 → 8 taken 2324 times.
✗ Branch 7 → 12 not taken.
✓ Branch 9 → 10 taken 2324 times.
✗ Branch 9 → 12 not taken.
99701 if (command.name != QLatin1String("require") || command.hasBlock || !command.tests.isEmpty() ||
572
5/6
✓ Branch 4 → 5 taken 2324 times.
✓ Branch 4 → 12 taken 92729 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2324 times.
✓ Branch 14 → 15 taken 92729 times.
✓ Branch 14 → 16 taken 2324 times.
99701 command.arguments.size() != 1 || command.arguments.first().kind != Argument::Kind::StringList)
573 92729 return false;
574
1/1
✓ Branch 17 → 18 taken 2324 times.
2324 *extensions = command.arguments.first().values();
575 2324 return true;
576 }
577
578 3263 Style detectStyle(const Script &script, const QList<Block> &blocks) {
579 3263 Style style;
580
4/4
✓ Branch 5 → 6 taken 29 times.
✓ Branch 5 → 7 taken 3234 times.
✓ Branch 6 → 8 taken 29 times.
✓ Branch 7 → 8 taken 3234 times.
3263 style.lineEnd = script.source.contains("\r\n") ? QByteArray("\r\n") : QByteArray("\n");
581
2/2
✓ Branch 48 → 12 taken 25303 times.
✓ Branch 48 → 49 taken 391 times.
25694 for (const Command &command : script.commands) {
582
7/10
✓ Branch 15 → 16 taken 2872 times.
✓ Branch 15 → 21 taken 22431 times.
✓ Branch 16 → 17 taken 2872 times.
✗ Branch 16 → 21 not taken.
✓ Branch 18 → 19 taken 2872 times.
✗ Branch 18 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 2872 times.
✓ Branch 23 → 24 taken 22431 times.
✓ Branch 23 → 25 taken 2872 times.
28175 if (command.name != QLatin1String("if") || !command.hasBlock || command.block.isEmpty() ||
583 2872 command.tests.isEmpty())
584 22431 continue;
585 2872 const int inner = command.block.first().span.begin;
586 2872 const int lineStart = script.source.lastIndexOf('\n', inner - 1) + 1;
587
1/1
✓ Branch 27 → 28 taken 2872 times.
2872 const QByteArray indent = script.source.mid(lineStart, inner - lineStart);
588
7/11
✓ Branch 29 → 30 taken 2872 times.
✗ Branch 29 → 34 not taken.
✓ Branch 30 → 31 taken 2872 times.
✓ Branch 32 → 33 taken 2682 times.
✓ Branch 32 → 34 taken 190 times.
✓ Branch 35 → 36 taken 2872 times.
✗ Branch 35 → 37 not taken.
✓ Branch 37 → 38 taken 2682 times.
✓ Branch 37 → 39 taken 190 times.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 69 not taken.
2872 if (!indent.isEmpty() && indent.trimmed().isEmpty())
589 2682 style.indent = indent;
590 5744 const QByteArray between = script.source.mid(command.tests.first().span.end,
591
1/1
✓ Branch 41 → 42 taken 2872 times.
2872 command.blockSpan.begin - command.tests.first().span.end);
592
1/1
✓ Branch 42 → 43 taken 2872 times.
2872 style.braceOnOwnLine = between.contains('\n');
593 2872 break;
594 2872 }
595 3263 int rules = 0;
596 3263 int withStop = 0;
597
2/2
✓ Branch 58 → 51 taken 95192 times.
✓ Branch 58 → 59 taken 3263 times.
98455 for (const Block &block : blocks) {
598
2/2
✓ Branch 52 → 53 taken 23063 times.
✓ Branch 52 → 54 taken 72129 times.
95192 if (block.kind != BlockKind::Rule)
599 23063 continue;
600 72129 ++rules;
601
2/2
✓ Branch 54 → 55 taken 63921 times.
✓ Branch 54 → 56 taken 8208 times.
72129 if (block.rule.stop)
602 63921 ++withStop;
603 }
604
4/4
✓ Branch 59 → 60 taken 2759 times.
✓ Branch 59 → 62 taken 504 times.
✓ Branch 60 → 61 taken 1196 times.
✓ Branch 60 → 62 taken 1563 times.
3263 style.stopByDefault = rules > 0 && withStop * 2 > rules;
605 3263 return style;
606 ✗ }
607
608 20390 bool endsTerminal(const Command &command) {
609
6/6
✓ Branch 4 → 5 taken 20389 times.
✓ Branch 4 → 11 taken 1 time.
✓ Branch 7 → 8 taken 20340 times.
✓ Branch 7 → 11 taken 49 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 20339 times.
40730 return command.name == QLatin1String("stop") || command.name == QLatin1String("keep") ||
610 40730 command.name == QLatin1String("discard");
611 }
612
613 } // namespace
614
615 3314 Document decode(const QByteArray &source) {
616
1/1
✓ Branch 2 → 3 taken 3314 times.
3314 Document document;
617 3314 document.source = source;
618
1/1
✓ Branch 4 → 5 taken 3314 times.
3314 const Script script = parse(source);
619
2/2
✓ Branch 6 → 7 taken 51 times.
✓ Branch 6 → 22 taken 3263 times.
3314 if (!script.isValid()) {
620 51 document.readOnly = true;
621 51 document.error = script.error;
622 51 Block block;
623 51 block.kind = BlockKind::Advanced;
624 51 block.original = source;
625 51 block.originalIndex = 0;
626 51 block.line = 1;
627
1/1
✓ Branch 9 → 10 taken 51 times.
51 document.blocks.append(block);
628
5/5
✓ Branch 10 → 11 taken 51 times.
✓ Branch 14 → 15 taken 45 times.
✓ Branch 15 → 16 taken 45 times.
✓ Branch 19 → 12 taken 45 times.
✓ Branch 19 → 20 taken 51 times.
96 qCInfo(lcSieveCodec) << "Script does not parse; opened read-only";
629 51 return document;
630 51 }
631 3263 int cursor = 0;
632
1/1
✓ Branch 22 → 23 taken 3263 times.
3263 const QList<Group> groups = groupCommands(script.commands);
633
2/2
✓ Branch 81 → 25 taken 95081 times.
✓ Branch 81 → 82 taken 3263 times.
98344 for (const Group &group : groups) {
634 95081 const Command &first = script.commands.at(group.first);
635 95081 const Command &last = script.commands.at(group.last);
636 95081 const int end = blockEnd(source, last.span.end);
637 95081 Block block;
638
1/1
✓ Branch 29 → 30 taken 95081 times.
95081 block.original = source.mid(cursor, end - cursor);
639 95081 block.originalIndex = document.blocks.size();
640 95081 block.line = first.line;
641
1/1
✓ Branch 33 → 34 taken 95081 times.
95081 const QByteArray leading = source.mid(cursor, first.span.begin - cursor);
642 95081 QByteArray prefix;
643 95081 QString name;
644
3/3
✓ Branch 34 → 35 taken 95081 times.
✓ Branch 35 → 36 taken 41912 times.
✓ Branch 35 → 38 taken 53169 times.
95081 if (splitMarker(leading, &prefix, &name)) {
645 41912 block.named = true;
646 41912 block.name = name;
647 41912 block.leading = prefix;
648 } else {
649 53169 block.leading = leading;
650 }
651 95081 QStringList extensions;
652
7/7
✓ Branch 39 → 40 taken 95053 times.
✓ Branch 39 → 43 taken 28 times.
✓ Branch 40 → 41 taken 95053 times.
✓ Branch 41 → 42 taken 2324 times.
✓ Branch 41 → 43 taken 92729 times.
✓ Branch 44 → 45 taken 2324 times.
✓ Branch 44 → 46 taken 92757 times.
95081 if (group.first == group.last && isRequireCommand(first, &extensions)) {
653 2324 block.kind = BlockKind::Require;
654 2324 block.extensions = extensions;
655
7/7
✓ Branch 46 → 47 taken 92729 times.
✓ Branch 46 → 50 taken 28 times.
✓ Branch 47 → 48 taken 92729 times.
✓ Branch 48 → 49 taken 72338 times.
✓ Branch 48 → 50 taken 20391 times.
✓ Branch 51 → 52 taken 72338 times.
✓ Branch 51 → 53 taken 20419 times.
92757 } else if (group.first == group.last && decodeIfBlock(source, first, &block)) {
656 72338 block.rule.name = block.name;
657
2/2
✓ Branch 56 → 57 taken 38 times.
✓ Branch 56 → 66 taken 20353 times.
20391 } else if (group.first == group.last && first.name == QLatin1String("vacation") &&
658
5/8
✓ Branch 58 → 59 taken 38 times.
✓ Branch 59 → 60 taken 38 times.
✓ Branch 60 → 61 taken 38 times.
✗ Branch 60 → 66 not taken.
✓ Branch 69 → 70 taken 29 times.
✓ Branch 69 → 71 taken 20390 times.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
20457 !hasComment(source.mid(first.span.begin, first.span.length())) &&
659
9/10
✓ Branch 53 → 54 taken 20391 times.
✓ Branch 53 → 66 taken 28 times.
✓ Branch 61 → 62 taken 38 times.
✓ Branch 62 → 63 taken 29 times.
✓ Branch 62 → 66 taken 9 times.
✓ Branch 63 → 64 taken 29 times.
✓ Branch 64 → 65 taken 29 times.
✗ Branch 64 → 66 not taken.
✓ Branch 67 → 68 taken 38 times.
✓ Branch 67 → 69 taken 20381 times.
40810 decodeVacationCommand(first, &block.vacation) && representable(block.vacation)) {
660 29 block.kind = BlockKind::Vacation;
661 } else {
662 20390 block.kind = BlockKind::Advanced;
663 20390 block.terminal = endsTerminal(last);
664 }
665
1/1
✓ Branch 73 → 74 taken 95081 times.
95081 document.blocks.append(block);
666 95081 cursor = end;
667 95081 }
668
2/2
✓ Branch 83 → 84 taken 111 times.
✓ Branch 83 → 91 taken 3152 times.
3263 if (cursor < source.size()) {
669 111 Block tail;
670 111 tail.kind = BlockKind::Comment;
671
1/1
✓ Branch 84 → 85 taken 111 times.
111 tail.original = source.mid(cursor);
672 111 tail.originalIndex = document.blocks.size();
673
1/1
✓ Branch 88 → 89 taken 111 times.
111 document.blocks.append(tail);
674 111 }
675
1/1
✓ Branch 91 → 92 taken 3263 times.
3263 document.style = detectStyle(script, document.blocks);
676
8/8
✓ Branch 94 → 95 taken 3263 times.
✓ Branch 98 → 99 taken 2782 times.
✓ Branch 99 → 100 taken 2782 times.
✓ Branch 101 → 102 taken 2782 times.
✓ Branch 102 → 103 taken 2782 times.
✓ Branch 104 → 105 taken 2782 times.
✓ Branch 109 → 96 taken 2782 times.
✓ Branch 109 → 110 taken 3263 times.
6045 qCDebug(lcSieveCodec) << "Decoded" << document.blocks.size() << "blocks from" << source.size()
677
1/1
✓ Branch 105 → 106 taken 2782 times.
2782 << "bytes";
678 3263 return document;
679 3314 }
680
681 // --- encoding -----------------------------------------------------------------
682
683 namespace {
684
685 636 bool extensionPresent(const QSet<QString> &present, const QString &extension) {
686
2/2
✓ Branch 3 → 4 taken 363 times.
✓ Branch 3 → 5 taken 273 times.
636 if (present.contains(extension))
687 363 return true;
688 // Legacy "imapflags" offers the same flag commands (reference server).
689
8/8
✓ Branch 7 → 8 taken 31 times.
✓ Branch 7 → 14 taken 242 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 29 times.
✓ Branch 16 → 17 taken 31 times.
✓ Branch 16 → 18 taken 242 times.
✓ Branch 18 → 19 taken 31 times.
✓ Branch 18 → 20 taken 242 times.
304 return extension == QLatin1String("imap4flags") && present.contains(QStringLiteral("imapflags"));
690 }
691
692 13 QByteArray patchRequire(const QByteArray &bytes, const QStringList &missing) {
693 13 return addRequirements(bytes, missing);
694 }
695
696 22789 QByteArray blockBytes(const Block &block, const Style &style) {
697
2/2
✓ Branch 2 → 3 taken 22231 times.
✓ Branch 2 → 4 taken 558 times.
22789 if (!block.dirty)
698 22231 return block.original;
699
2/2
✓ Branch 4 → 5 taken 542 times.
✓ Branch 4 → 10 taken 16 times.
558 if (block.kind == BlockKind::Rule)
700
2/2
✓ Branch 5 → 6 taken 542 times.
✓ Branch 6 → 7 taken 542 times.
542 return block.leading + renderRule(block.rule, style);
701
1/2
✓ Branch 10 → 11 taken 16 times.
✗ Branch 10 → 16 not taken.
16 if (block.kind == BlockKind::Vacation)
702
2/2
✓ Branch 11 → 12 taken 16 times.
✓ Branch 12 → 13 taken 16 times.
16 return block.leading + renderVacation(block.vacation, block.name, style);
703 ✗ return block.original;
704 }
705
706 22 void collectFileInto(const QList<Command> &commands, QList<StringValue> *targets) {
707
2/2
✓ Branch 24 → 4 taken 18 times.
✓ Branch 24 → 25 taken 22 times.
40 for (const Command &command : commands) {
708
1/2
✓ Branch 9 → 10 taken 4 times.
✗ Branch 9 → 16 not taken.
22 if (command.name == QLatin1String("fileinto") && !command.arguments.isEmpty() &&
709
6/8
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 16 taken 14 times.
✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 16 not taken.
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 21 taken 14 times.
26 command.arguments.last().kind == Argument::Kind::StringList &&
710 4 command.arguments.last().strings.size() == 1)
711
1/1
✓ Branch 20 → 21 taken 4 times.
4 targets->append(command.arguments.last().strings.first());
712
1/1
✓ Branch 21 → 22 taken 18 times.
18 collectFileInto(command.block, targets);
713 }
714 22 }
715
716 20 bool renamedTarget(const QString &target, const QString &oldMailbox, const QString &newMailbox,
717 QChar delimiter, QString *result) {
718
2/2
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 6 taken 12 times.
20 if (target == oldMailbox) {
719 8 *result = newMailbox;
720 8 return true;
721 }
722
1/1
✓ Branch 6 → 7 taken 12 times.
12 const QString prefix = oldMailbox + delimiter;
723
3/3
✓ Branch 7 → 8 taken 12 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 4 times.
12 if (!target.startsWith(prefix))
724 8 return false;
725
3/3
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
4 *result = newMailbox + delimiter + target.mid(prefix.size());
726 4 return true;
727 12 }
728
729 } // namespace
730
731 902 QByteArray encode(const Document &document) {
732
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 895 times.
902 if (document.readOnly)
733 7 return document.source;
734
1/1
✓ Branch 4 → 5 taken 895 times.
895 const QStringList required = document.requiredExtensions();
735
1/1
✓ Branch 7 → 8 taken 895 times.
895 const QSet<QString> present(required.cbegin(), required.cend());
736 895 QStringList missing;
737
2/2
✓ Branch 34 → 10 taken 22789 times.
✓ Branch 34 → 35 taken 895 times.
23684 for (const Block &block : document.blocks) {
738
2/2
✓ Branch 11 → 12 taken 22231 times.
✓ Branch 11 → 13 taken 558 times.
22789 if (!block.dirty)
739 22231 continue;
740
2/2
✓ Branch 13 → 14 taken 16 times.
✓ Branch 13 → 15 taken 542 times.
558 const QStringList needed = block.kind == BlockKind::Vacation ? extensionsFor(block.vacation)
741
2/2
✓ Branch 14 → 16 taken 16 times.
✓ Branch 15 → 16 taken 542 times.
558 : extensionsFor(block.rule);
742
2/2
✓ Branch 29 → 18 taken 636 times.
✓ Branch 29 → 30 taken 558 times.
1194 for (const QString &extension : needed)
743
7/7
✓ Branch 19 → 20 taken 636 times.
✓ Branch 20 → 21 taken 271 times.
✓ Branch 20 → 24 taken 365 times.
✓ Branch 22 → 23 taken 262 times.
✓ Branch 22 → 24 taken 9 times.
✓ Branch 25 → 26 taken 262 times.
✓ Branch 25 → 27 taken 374 times.
636 if (!extensionPresent(present, extension) && !missing.contains(extension))
744
1/1
✓ Branch 26 → 27 taken 262 times.
262 missing.append(extension);
745 558 }
746 // S106-096: only a require block in the leading run (before any command)
747 // gains the missing extensions; RFC 5228 §3.2 forbids using an extension
748 // before its require, so a require further down gets a new line on top.
749 895 int patchable = -1;
750
2/2
✓ Branch 43 → 36 taken 895 times.
✓ Branch 43 → 44 taken 1 time.
896 for (int i = 0; i < document.blocks.size(); ++i) {
751 895 const BlockKind kind = document.blocks.at(i).kind;
752
2/2
✓ Branch 37 → 38 taken 624 times.
✓ Branch 37 → 39 taken 271 times.
895 if (kind == BlockKind::Require) {
753 624 patchable = i;
754 624 break;
755 }
756
2/2
✓ Branch 39 → 40 taken 270 times.
✓ Branch 39 → 41 taken 1 time.
271 if (kind != BlockKind::Comment)
757 270 break;
758 }
759 895 QByteArray out;
760 895 bool requirePatched = false;
761 895 int previousIndex = -2;
762 895 bool previousDirty = false;
763
2/2
✓ Branch 80 → 45 taken 22789 times.
✓ Branch 80 → 81 taken 895 times.
23684 for (int i = 0; i < document.blocks.size(); ++i) {
764 22789 const Block &block = document.blocks.at(i);
765
1/1
✓ Branch 46 → 47 taken 22789 times.
22789 QByteArray bytes = blockBytes(block, document.style);
766
6/6
✓ Branch 47 → 48 taken 624 times.
✓ Branch 47 → 51 taken 22165 times.
✓ Branch 49 → 50 taken 13 times.
✓ Branch 49 → 51 taken 611 times.
✓ Branch 52 → 53 taken 13 times.
✓ Branch 52 → 57 taken 22776 times.
22789 if (i == patchable && !missing.isEmpty()) {
767
1/1
✓ Branch 53 → 54 taken 13 times.
13 bytes = patchRequire(bytes, missing);
768 13 requirePatched = true;
769 }
770
5/6
✓ Branch 57 → 58 taken 22231 times.
✓ Branch 57 → 62 taken 558 times.
✓ Branch 58 → 59 taken 21975 times.
✓ Branch 58 → 62 taken 256 times.
✓ Branch 59 → 60 taken 21975 times.
✗ Branch 59 → 62 not taken.
44764 const bool adjacent = !block.dirty && !previousDirty && block.originalIndex >= 0 &&
771
2/2
✓ Branch 60 → 61 taken 20969 times.
✓ Branch 60 → 62 taken 1006 times.
21975 block.originalIndex == previousIndex + 1;
772
9/9
✓ Branch 64 → 65 taken 21894 times.
✓ Branch 64 → 69 taken 895 times.
✓ Branch 65 → 66 taken 21894 times.
✓ Branch 66 → 67 taken 5 times.
✓ Branch 66 → 69 taken 21889 times.
✓ Branch 67 → 68 taken 4 times.
✓ Branch 67 → 69 taken 1 time.
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 72 taken 22785 times.
22789 if (!out.isEmpty() && !out.endsWith('\n') && !adjacent)
773
1/1
✓ Branch 71 → 72 taken 4 times.
4 out += document.style.lineEnd;
774
1/1
✓ Branch 72 → 73 taken 22789 times.
22789 out += bytes;
775 22789 previousIndex = block.originalIndex;
776
3/4
✓ Branch 73 → 74 taken 22231 times.
✓ Branch 73 → 75 taken 558 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 22231 times.
22789 previousDirty = block.dirty || block.originalIndex < 0;
777 22789 }
778
6/6
✓ Branch 82 → 83 taken 212 times.
✓ Branch 82 → 85 taken 683 times.
✓ Branch 83 → 84 taken 199 times.
✓ Branch 83 → 85 taken 13 times.
✓ Branch 86 → 87 taken 199 times.
✓ Branch 86 → 114 taken 696 times.
895 if (!missing.isEmpty() && !requirePatched) {
779 199 QStringList sorted = missing;
780
1/1
✓ Branch 88 → 89 taken 199 times.
199 QByteArray list = "[";
781
2/2
✓ Branch 103 → 90 taken 242 times.
✓ Branch 103 → 104 taken 199 times.
441 for (int i = 0; i < sorted.size(); ++i)
782
6/6
✓ Branch 90 → 91 taken 242 times.
✓ Branch 92 → 93 taken 242 times.
✓ Branch 93 → 94 taken 43 times.
✓ Branch 93 → 95 taken 199 times.
✓ Branch 96 → 97 taken 242 times.
✓ Branch 97 → 98 taken 242 times.
242 list += (i > 0 ? "," : "") + stringLiteral(sorted.at(i));
783
4/4
✓ Branch 104 → 105 taken 199 times.
✓ Branch 105 → 106 taken 199 times.
✓ Branch 106 → 107 taken 199 times.
✓ Branch 107 → 108 taken 199 times.
199 out.prepend("require " + list + "];" + document.style.lineEnd);
784 199 }
785 895 return out;
786 895 }
787
788 234 int insertRule(Document *document, const Rule &rule) {
789 234 Block block;
790 234 block.kind = BlockKind::Rule;
791 234 block.rule = rule;
792 234 block.name = rule.name;
793 234 block.named = !rule.name.isEmpty();
794 234 block.dirty = true;
795 234 int position = document->blocks.size();
796
6/6
✓ Branch 8 → 9 taken 30 times.
✓ Branch 8 → 12 taken 206 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 28 times.
✓ Branch 13 → 7 taken 2 times.
✓ Branch 13 → 14 taken 234 times.
236 while (position > 0 && document->blocks.at(position - 1).kind == BlockKind::Comment)
797 2 --position;
798
2/2
✓ Branch 14 → 15 taken 28 times.
✓ Branch 14 → 19 taken 206 times.
234 if (position > 0) {
799 28 const Block &previous = document->blocks.at(position - 1);
800
4/4
✓ Branch 16 → 17 taken 5 times.
✓ Branch 16 → 19 taken 23 times.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 1 time.
28 if (previous.kind == BlockKind::Advanced && previous.terminal)
801 4 --position;
802 }
803
1/1
✓ Branch 19 → 20 taken 234 times.
234 document->blocks.insert(position, block);
804 234 return position;
805 234 }
806
807 12 int placeVacation(Document *document, const Vacation &vacation) {
808
2/2
✓ Branch 18 → 3 taken 12 times.
✓ Branch 18 → 19 taken 11 times.
23 for (int i = 0; i < document->blocks.size(); ++i) {
809
1/1
✓ Branch 3 → 4 taken 12 times.
12 Block &block = document->blocks[i];
810
2/2
✓ Branch 4 → 5 taken 11 times.
✓ Branch 4 → 6 taken 1 time.
12 if (block.kind != BlockKind::Vacation)
811 11 continue;
812 1 block.vacation = vacation;
813 1 block.dirty = true;
814
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 15 taken 1 time.
1 if (!block.named) {
815 ✗ block.named = true;
816 ✗ block.name = QStringLiteral("Out of office");
817 }
818 1 return i;
819 }
820 11 Block block;
821 11 block.kind = BlockKind::Vacation;
822 11 block.vacation = vacation;
823 11 block.name = QStringLiteral("Out of office");
824 11 block.named = true;
825 11 block.dirty = true;
826 11 int position = 0;
827
4/4
✓ Branch 29 → 30 taken 6 times.
✓ Branch 29 → 33 taken 8 times.
✓ Branch 34 → 27 taken 3 times.
✓ Branch 34 → 35 taken 11 times.
20 while (position < document->blocks.size() &&
828
2/2
✓ Branch 31 → 32 taken 3 times.
✓ Branch 31 → 33 taken 3 times.
6 document->blocks.at(position).kind == BlockKind::Require)
829 3 ++position;
830
1/1
✓ Branch 35 → 36 taken 11 times.
11 document->blocks.insert(position, block);
831 11 return position;
832 11 }
833
834 7 int renameMailbox(Document *document, const QString &oldMailbox, const QString &newMailbox,
835 QChar delimiter) {
836
6/8
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 7 taken 1 time.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 6 times.
7 if (document->readOnly || oldMailbox.isEmpty() || oldMailbox == newMailbox)
837 1 return 0;
838 6 int changed = 0;
839
4/4
✓ Branch 11 → 12 taken 6 times.
✓ Branch 12 → 13 taken 6 times.
✓ Branch 89 → 14 taken 26 times.
✓ Branch 89 → 90 taken 6 times.
32 for (Block &block : document->blocks) {
840
2/2
✓ Branch 15 → 16 taken 16 times.
✓ Branch 15 → 35 taken 10 times.
26 if (block.kind == BlockKind::Rule) {
841 16 bool touched = false;
842
4/4
✓ Branch 16 → 17 taken 16 times.
✓ Branch 17 → 18 taken 16 times.
✓ Branch 31 → 19 taken 16 times.
✓ Branch 31 → 32 taken 16 times.
32 for (Action &action : block.rule.actions) {
843 16 QString renamed;
844
3/4
✓ Branch 20 → 21 taken 16 times.
✗ Branch 20 → 24 not taken.
✓ Branch 25 → 26 taken 8 times.
✓ Branch 25 → 28 taken 8 times.
32 if (action.kind == Action::Kind::FileInto &&
845
3/3
✓ Branch 21 → 22 taken 16 times.
✓ Branch 22 → 23 taken 8 times.
✓ Branch 22 → 24 taken 8 times.
16 renamedTarget(action.target, oldMailbox, newMailbox, delimiter, &renamed)) {
846 8 action.target = renamed;
847 8 touched = true;
848 }
849 16 }
850
2/2
✓ Branch 32 → 33 taken 8 times.
✓ Branch 32 → 34 taken 8 times.
16 if (touched) {
851 8 block.dirty = true;
852 8 ++changed;
853 }
854 22 continue;
855 16 }
856
5/6
✓ Branch 35 → 36 taken 4 times.
✓ Branch 35 → 38 taken 6 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 4 times.
✓ Branch 40 → 41 taken 6 times.
✓ Branch 40 → 42 taken 4 times.
10 if (block.kind != BlockKind::Advanced || block.original.isEmpty())
857 6 continue;
858
1/1
✓ Branch 42 → 43 taken 4 times.
4 const Script script = parse(block.original);
859
1/2
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 4 times.
4 if (!script.isValid())
860 ✗ continue;
861 4 QList<StringValue> targets;
862
1/1
✓ Branch 46 → 47 taken 4 times.
4 collectFileInto(script.commands, &targets);
863 4 QByteArray patched = block.original;
864 4 bool touched = false;
865
2/2
✓ Branch 75 → 50 taken 4 times.
✓ Branch 75 → 76 taken 4 times.
8 for (int i = targets.size() - 1; i >= 0; --i) {
866 4 QString renamed;
867
2/3
✓ Branch 51 → 52 taken 4 times.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 4 times.
4 if (!renamedTarget(targets.at(i).value, oldMailbox, newMailbox, delimiter, &renamed))
868 ✗ continue;
869 4 const Span span = targets.at(i).span;
870
6/6
✓ Branch 55 → 56 taken 4 times.
✓ Branch 56 → 57 taken 4 times.
✓ Branch 57 → 58 taken 4 times.
✓ Branch 58 → 59 taken 4 times.
✓ Branch 59 → 60 taken 4 times.
✓ Branch 60 → 61 taken 4 times.
4 patched = patched.left(span.begin) + stringLiteral(renamed) + patched.mid(span.end);
871 4 touched = true;
872 4 }
873
1/2
✓ Branch 76 → 77 taken 4 times.
✗ Branch 76 → 79 not taken.
4 if (touched) {
874 // Advanced blocks are patched in place and stay byte-copied.
875 4 block.original = patched;
876 4 ++changed;
877 }
878 4 }
879
7/7
✓ Branch 90 → 91 taken 6 times.
✓ Branch 94 → 95 taken 4 times.
✓ Branch 95 → 96 taken 4 times.
✓ Branch 96 → 97 taken 4 times.
✓ Branch 97 → 98 taken 4 times.
✓ Branch 101 → 92 taken 4 times.
✓ Branch 101 → 102 taken 6 times.
10 qCInfo(lcSieveCodec) << "Mailbox rename touched" << changed << "blocks";
880 6 return changed;
881 }
882
883 } // namespace Sieve
884