util/AddressListScan.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | // S106-209: the (address, display name) pairs of an address header such as | ||
| 3 | // From/To/Cc, for the search summary's sender facets. Header-only and pure: | ||
| 4 | // one linear scan, no regular expression — the former regex's lazy name | ||
| 5 | // part re-scanned the rest of a long '@'-free header from every position. | ||
| 6 | // Mail headers are untrusted; fuzzed by fuzz_address_list. | ||
| 7 | // | ||
| 8 | // Accepted forms, separated by ',' or ';' outside double quotes: | ||
| 9 | // "Quoted, Name" <a@b> Plain Name <a@b> <a@b> a@b | ||
| 10 | // Addresses are lower-cased; a token is an address when it has text on both | ||
| 11 | // sides of an '@' and holds no whitespace, '<', '>' or '"'. | ||
| 12 | |||
| 13 | #include <QList> | ||
| 14 | #include <QPair> | ||
| 15 | #include <QString> | ||
| 16 | #include <QStringView> | ||
| 17 | #include <algorithm> | ||
| 18 | |||
| 19 | namespace AddressListScan { | ||
| 20 | |||
| 21 | // Text on both sides of the first '@' (the address rule of both forms). | ||
| 22 | 549147 | inline bool hasTextAroundAt(QStringView token) { | |
| 23 | 549147 | const qsizetype at = token.indexOf(QLatin1Char('@')); | |
| 24 |
3/4✓ Branch 5 → 6 taken 27963 times.
✓ Branch 5 → 9 taken 521184 times.
✓ Branch 7 → 8 taken 27963 times.
✗ Branch 7 → 9 not taken.
|
549147 | return at > 0 && at < token.size() - 1; |
| 25 | } | ||
| 26 | |||
| 27 | 1104625 | inline bool endsBareToken(QChar ch) { | |
| 28 |
7/8✓ Branch 3 → 4 taken 604622 times.
✓ Branch 3 → 16 taken 500003 times.
✓ Branch 7 → 8 taken 604621 times.
✓ Branch 7 → 16 taken 1 time.
✓ Branch 11 → 12 taken 604620 times.
✓ Branch 11 → 16 taken 1 time.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 604620 times.
|
1104625 | return ch.isSpace() || ch == QLatin1Char('<') || ch == QLatin1Char('>') || ch == QLatin1Char('"'); |
| 29 | } | ||
| 30 | |||
| 31 | // Every maximal run without whitespace, <, > or " that has text on both | ||
| 32 | // sides of an '@' is a bare address. | ||
| 33 | 27961 | inline void appendBare(QStringView text, QList<QPair<QString, QString>> &result) { | |
| 34 | 27961 | qsizetype start = 0; | |
| 35 |
2/2✓ Branch 29 → 3 taken 1132586 times.
✓ Branch 29 → 30 taken 27961 times.
|
1160547 | for (qsizetype i = 0; i <= text.size(); ++i) { |
| 36 |
6/6✓ Branch 4 → 5 taken 1104625 times.
✓ Branch 4 → 9 taken 27961 times.
✓ Branch 7 → 8 taken 604620 times.
✓ Branch 7 → 9 taken 500005 times.
✓ Branch 10 → 11 taken 604620 times.
✓ Branch 10 → 12 taken 527966 times.
|
1132586 | if (i < text.size() && !endsBareToken(text.at(i))) continue; |
| 37 | 527966 | const QStringView token = text.mid(start, i - start); | |
| 38 |
5/5✓ Branch 14 → 15 taken 6782 times.
✓ Branch 14 → 25 taken 521184 times.
✓ Branch 15 → 16 taken 6782 times.
✓ Branch 16 → 17 taken 6782 times.
✓ Branch 19 → 20 taken 6782 times.
|
527966 | if (hasTextAroundAt(token)) result.append({token.toString().toLower(), QString()}); |
| 39 | 527966 | start = i + 1; | |
| 40 | } | ||
| 41 | 27961 | } | |
| 42 | |||
| 43 | // The next '<' outside double quotes at or after `from`, or -1. | ||
| 44 | 27961 | inline qsizetype nextAngle(QStringView part, qsizetype from) { | |
| 45 | 27961 | bool quoted = false; | |
| 46 |
2/2✓ Branch 21 → 3 taken 1351675 times.
✓ Branch 21 → 22 taken 6780 times.
|
1358455 | for (qsizetype i = from; i < part.size(); ++i) { |
| 47 |
2/2✓ Branch 7 → 8 taken 12306 times.
✓ Branch 7 → 9 taken 1339369 times.
|
1351675 | if (part.at(i) == QLatin1Char('"')) quoted = !quoted; |
| 48 |
6/6✓ Branch 9 → 10 taken 1283991 times.
✓ Branch 9 → 16 taken 55378 times.
✓ Branch 14 → 15 taken 21181 times.
✓ Branch 14 → 16 taken 1262810 times.
✓ Branch 17 → 18 taken 21181 times.
✓ Branch 17 → 19 taken 1318188 times.
|
1339369 | else if (!quoted && part.at(i) == QLatin1Char('<')) return i; |
| 49 | } | ||
| 50 | 6780 | return -1; | |
| 51 | } | ||
| 52 | |||
| 53 | // One part between separators: "Name <a@b>" (several may follow each | ||
| 54 | // other) and bare a@b tokens. Every step moves `position` forward. | ||
| 55 | 27959 | inline void appendPart(QStringView part, QList<QPair<QString, QString>> &result) { | |
| 56 | 27959 | qsizetype position = 0; | |
| 57 |
2/2✓ Branch 86 → 3 taken 27961 times.
✓ Branch 86 → 87 taken 21179 times.
|
49140 | while (position < part.size()) { |
| 58 | 27961 | const qsizetype open = nextAngle(part, position); | |
| 59 |
2/2✓ Branch 4 → 5 taken 21181 times.
✓ Branch 4 → 8 taken 6780 times.
|
27961 | const qsizetype close = open < 0 ? -1 : part.indexOf(QLatin1Char('>'), open + 1); |
| 60 |
2/2✓ Branch 9 → 10 taken 6780 times.
✓ Branch 9 → 11 taken 21181 times.
|
27961 | if (close < 0) break; |
| 61 | 21181 | const QStringView inside = part.mid(open + 1, close - open - 1).trimmed(); | |
| 62 | // "<@@b>" has no local part: not an address (fuzz_address_list). | ||
| 63 |
1/2✓ Branch 14 → 15 taken 21181 times.
✗ Branch 14 → 20 not taken.
|
42362 | const bool valid = hasTextAroundAt(inside) && |
| 64 |
3/3✓ Branch 17 → 18 taken 21181 times.
✓ Branch 18 → 19 taken 21180 times.
✓ Branch 18 → 20 taken 1 time.
|
21181 | std::none_of(inside.begin(), inside.end(), |
| 65 |
3/4✓ Branch 3 → 4 taken 352335 times.
✓ Branch 3 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 352335 times.
|
352336 | [](QChar ch) { return ch.isSpace() || ch == QLatin1Char('<'); }); |
| 66 | 21181 | const QStringView name = part.mid(position, open - position).trimmed(); | |
| 67 |
2/2✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 26 taken 21180 times.
|
21181 | if (!valid) { |
| 68 |
1/1✓ Branch 25 → 84 taken 1 time.
|
1 | appendBare(part.mid(position, close + 1 - position), result); |
| 69 |
7/8✓ Branch 27 → 28 taken 21178 times.
✓ Branch 27 → 37 taken 2 times.
✓ Branch 31 → 32 taken 6153 times.
✓ Branch 31 → 37 taken 15025 times.
✓ Branch 35 → 36 taken 6153 times.
✗ Branch 35 → 37 not taken.
✓ Branch 38 → 39 taken 6153 times.
✓ Branch 38 → 52 taken 15027 times.
|
21180 | } else if (name.size() >= 2 && name.startsWith(QLatin1Char('"')) && name.endsWith(QLatin1Char('"'))) { |
| 70 |
4/4✓ Branch 39 → 40 taken 6153 times.
✓ Branch 40 → 41 taken 6153 times.
✓ Branch 44 → 45 taken 6153 times.
✓ Branch 46 → 47 taken 6153 times.
|
6153 | result.append({inside.toString().toLower(), name.mid(1, name.size() - 2).trimmed().toString()}); |
| 71 |
5/6✓ Branch 55 → 56 taken 15026 times.
✓ Branch 55 → 60 taken 1 time.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 15026 times.
✓ Branch 62 → 63 taken 1 time.
✓ Branch 62 → 74 taken 15026 times.
|
15027 | } else if (name.contains(QLatin1Char('@')) || name.contains(QLatin1Char('"'))) { |
| 72 | // An unquoted name cannot hold '@' or '"': it is an address of its own. | ||
| 73 |
1/1✓ Branch 63 → 64 taken 1 time.
|
1 | appendBare(name, result); |
| 74 |
3/3✓ Branch 64 → 65 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 68 → 69 taken 1 time.
|
1 | result.append({inside.toString().toLower(), QString()}); |
| 75 | } else { | ||
| 76 |
4/4✓ Branch 74 → 75 taken 15026 times.
✓ Branch 75 → 76 taken 15026 times.
✓ Branch 76 → 77 taken 15026 times.
✓ Branch 78 → 79 taken 15026 times.
|
15026 | result.append({inside.toString().toLower(), name.toString()}); |
| 77 | } | ||
| 78 | 21181 | position = close + 1; | |
| 79 | } | ||
| 80 | 27959 | appendBare(part.mid(position), result); | |
| 81 | 27959 | } | |
| 82 | |||
| 83 | // The caller bounds `header` (the summary reads at most 4096 characters). | ||
| 84 | 28220 | inline QList<QPair<QString, QString>> parse(QStringView header) { | |
| 85 | 28220 | QList<QPair<QString, QString>> result; | |
| 86 | 28220 | bool quoted = false; | |
| 87 | 28220 | qsizetype start = 0; | |
| 88 |
2/2✓ Branch 35 → 3 taken 2754568 times.
✓ Branch 35 → 36 taken 28220 times.
|
2782788 | for (qsizetype i = 0; i <= header.size(); ++i) { |
| 89 |
2/2✓ Branch 4 → 5 taken 2726348 times.
✓ Branch 4 → 26 taken 28220 times.
|
2754568 | if (i < header.size()) { |
| 90 | 2726348 | const QChar ch = header.at(i); | |
| 91 |
2/2✓ Branch 9 → 10 taken 12306 times.
✓ Branch 9 → 11 taken 2714042 times.
|
2726348 | if (ch == QLatin1Char('"')) quoted = !quoted; |
| 92 |
8/8✓ Branch 11 → 12 taken 2664817 times.
✓ Branch 11 → 20 taken 61531 times.
✓ Branch 15 → 16 taken 2664187 times.
✓ Branch 15 → 21 taken 630 times.
✓ Branch 19 → 20 taken 2664185 times.
✓ Branch 19 → 21 taken 2 times.
✓ Branch 22 → 23 taken 2725716 times.
✓ Branch 22 → 24 taken 632 times.
|
2726348 | if (quoted || (ch != QLatin1Char(',') && ch != QLatin1Char(';'))) continue; |
| 93 | } | ||
| 94 | 28852 | const QStringView part = header.mid(start, i - start); | |
| 95 |
3/3✓ Branch 30 → 31 taken 27959 times.
✓ Branch 30 → 32 taken 893 times.
✓ Branch 31 → 32 taken 27959 times.
|
28852 | if (part.contains(QLatin1Char('@'))) appendPart(part, result); |
| 96 | 28852 | start = i + 1; | |
| 97 | } | ||
| 98 | 28220 | return result; | |
| 99 | ✗ | } | |
| 100 | |||
| 101 | } // namespace AddressListScan | ||
| 102 |