service/CalDavSchedulingXml.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Sprint 108 S108.8.4/8.5 (RFC 6638): the scheduling half of CalDavXml — | ||
| 2 | // the principal's outbox and addresses, the VFREEBUSY request and the | ||
| 3 | // schedule-response (declared in CalDavXml.h; fuzz target freebusy). | ||
| 4 | #include "CalDavXml.h" | ||
| 5 | #include "DavXmlHelper.h" | ||
| 6 | #include "data/ICalEventCodec.h" | ||
| 7 | #include "data/ICalendar.h" | ||
| 8 | |||
| 9 | #include <QDomDocument> | ||
| 10 | #include <QTimeZone> | ||
| 11 | #include <algorithm> | ||
| 12 | |||
| 13 | using namespace DavXmlHelper; | ||
| 14 | |||
| 15 | namespace CalDavXml { | ||
| 16 | |||
| 17 | // ── Sprint 108: RFC 6638 scheduling ───────────────────────────────────── | ||
| 18 | |||
| 19 | namespace { | ||
| 20 | |||
| 21 | constexpr int kMaxSchedulingAddresses = 50; | ||
| 22 | constexpr int kMaxFreeBusyRecipients = 64; | ||
| 23 | constexpr int kMaxBusyIntervals = 2000; | ||
| 24 | constexpr qsizetype kMaxFreeBusyXml = 1024 * 1024; | ||
| 25 | |||
| 26 | 7 | QString mailtoAddress(const QString &value) { | |
| 27 |
1/1✓ Branch 2 → 3 taken 7 times.
|
7 | const QString text = value.trimmed(); |
| 28 |
3/3✓ Branch 4 → 5 taken 7 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 6 times.
|
7 | if (!text.startsWith(QLatin1String("mailto:"), Qt::CaseInsensitive)) return {}; |
| 29 |
3/3✓ Branch 7 → 8 taken 6 times.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
|
6 | const QString address = text.mid(7).trimmed().toLower(); |
| 30 |
3/5✓ Branch 14 → 15 taken 6 times.
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 19 not taken.
✓ Branch 17 → 18 taken 6 times.
✗ Branch 17 → 19 not taken.
|
6 | return address.contains(QLatin1Char('@')) && address.size() <= 320 ? address : QString(); |
| 31 | 7 | } | |
| 32 | |||
| 33 | 10 | QDateTime utcValue(const QString &text) { | |
| 34 |
2/2✓ Branch 5 → 6 taken 10 times.
✓ Branch 6 → 7 taken 10 times.
|
10 | QDateTime value = QDateTime::fromString(text.trimmed(), QStringLiteral("yyyyMMdd'T'HHmmss'Z'")); |
| 35 |
1/1✓ Branch 11 → 12 taken 10 times.
|
10 | value.setTimeZone(QTimeZone::UTC); |
| 36 | 10 | return value; | |
| 37 | ✗ | } | |
| 38 | |||
| 39 | } // namespace | ||
| 40 | |||
| 41 | 3 | Scheduling parseScheduling(const QDomDocument &doc) { | |
| 42 | 3 | Scheduling scheduling; | |
| 43 |
1/1✓ Branch 2 → 3 taken 3 times.
|
3 | const QDomElement root = doc.documentElement(); |
| 44 |
1/1✓ Branch 6 → 7 taken 3 times.
|
3 | scheduling.outboxHref = hrefOfProperty(root, QStringLiteral("schedule-outbox-URL")); |
| 45 |
1/1✓ Branch 14 → 15 taken 3 times.
|
3 | const QDomElement set = findFirstElementByLocalName(root, QStringLiteral("calendar-user-address-set")); |
| 46 |
3/3✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 1 time.
|
3 | if (set.isNull()) return scheduling; |
| 47 |
6/6✓ Branch 20 → 21 taken 1 time.
✓ Branch 60 → 61 taken 3 times.
✓ Branch 61 → 62 taken 3 times.
✓ Branch 64 → 65 taken 4 times.
✓ Branch 65 → 22 taken 3 times.
✓ Branch 65 → 66 taken 1 time.
|
4 | for (QDomNode node = set.firstChild(); !node.isNull(); node = node.nextSibling()) { |
| 48 |
1/1✓ Branch 22 → 23 taken 3 times.
|
3 | const QDomElement element = node.toElement(); |
| 49 |
6/12✓ Branch 23 → 24 taken 3 times.
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 29 not taken.
✓ Branch 26 → 27 taken 3 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 3 times.
✓ Branch 31 → 32 taken 3 times.
✗ Branch 31 → 33 not taken.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 3 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
|
3 | if (element.isNull() || localNameOf(element) != QLatin1String("href")) continue; |
| 50 |
2/2✓ Branch 35 → 36 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
|
3 | const QString address = mailtoAddress(element.text()); |
| 51 |
7/7✓ Branch 39 → 40 taken 2 times.
✓ Branch 39 → 43 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 43 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 2 times.
✓ Branch 45 → 46 taken 1 time.
|
3 | if (!address.isEmpty() && !scheduling.addresses.contains(address)) scheduling.addresses.append(address); |
| 52 |
1/2✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 3 times.
|
3 | if (scheduling.addresses.size() >= kMaxSchedulingAddresses) break; |
| 53 | 4 | } | |
| 54 | 1 | return scheduling; | |
| 55 | 3 | } | |
| 56 | |||
| 57 | 4 | QByteArray freeBusyRequest(const QString &uid, const QString &organizer, const QStringList &attendees, | |
| 58 | const QDateTime &from, const QDateTime &to, const QDateTime &now) { | ||
| 59 | 12 | const auto stamp = [](const QDateTime &value) { | |
| 60 |
2/2✓ Branch 2 → 3 taken 12 times.
✓ Branch 6 → 7 taken 12 times.
|
24 | return value.toUTC().toString(QStringLiteral("yyyyMMdd'T'HHmmss'Z'")); |
| 61 | }; | ||
| 62 | 4 | QStringList lines{QStringLiteral("BEGIN:VCALENDAR"), QStringLiteral("VERSION:2.0"), | |
| 63 | 4 | QStringLiteral("PRODID:-//MailJD//MailJD Calendar//EN"), QStringLiteral("METHOD:REQUEST"), | |
| 64 | 8 | QStringLiteral("BEGIN:VFREEBUSY"), QStringLiteral("UID:") + uid, | |
| 65 | 12 | QStringLiteral("DTSTAMP:") + stamp(now), QStringLiteral("DTSTART:") + stamp(from), | |
| 66 | 8 | QStringLiteral("DTEND:") + stamp(to), | |
| 67 |
3/5✓ Branch 37 → 38 taken 4 times.
✓ Branch 44 → 45 taken 40 times.
✓ Branch 44 → 46 taken 4 times.
✗ Branch 116 → 117 not taken.
✗ Branch 116 → 118 not taken.
|
52 | QStringLiteral("ORGANIZER:mailto:") + organizer.trimmed().toLower()}; |
| 68 |
5/5✓ Branch 66 → 67 taken 4 times.
✓ Branch 67 → 68 taken 4 times.
✓ Branch 68 → 69 taken 4 times.
✓ Branch 85 → 70 taken 5 times.
✓ Branch 85 → 86 taken 4 times.
|
9 | for (const QString &attendee : attendees.mid(0, kMaxFreeBusyRecipients)) |
| 69 |
4/4✓ Branch 71 → 72 taken 5 times.
✓ Branch 72 → 73 taken 5 times.
✓ Branch 76 → 77 taken 5 times.
✓ Branch 77 → 78 taken 5 times.
|
14 | lines.append(QStringLiteral("ATTENDEE:mailto:") + attendee.trimmed().toLower()); |
| 70 |
2/2✓ Branch 90 → 91 taken 4 times.
✓ Branch 94 → 95 taken 4 times.
|
8 | lines << QStringLiteral("END:VFREEBUSY") << QStringLiteral("END:VCALENDAR"); |
| 71 | 4 | QString text; | |
| 72 |
6/6✓ Branch 99 → 100 taken 4 times.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 103 → 104 taken 53 times.
✓ Branch 104 → 105 taken 53 times.
✓ Branch 108 → 102 taken 53 times.
✓ Branch 108 → 109 taken 4 times.
|
57 | for (const QString &line : lines) text += ICal::foldLine(line); // each ends in CRLF |
| 73 |
1/1✓ Branch 109 → 110 taken 4 times.
|
8 | return text.toUtf8(); |
| 74 | 48 | } | |
| 75 | |||
| 76 | 5 | QList<FreeBusy> parseFreeBusyResponse(const QByteArray &xml, const QDateTime &from, const QDateTime &to) { | |
| 77 | 5 | QList<FreeBusy> out; | |
| 78 |
10/13✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 11 taken 1 time.
✓ Branch 4 → 5 taken 4 times.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 7 → 8 taken 4 times.
✗ Branch 7 → 11 not taken.
✓ Branch 8 → 9 taken 4 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 4 times.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 4 times.
|
5 | if (xml.size() > kMaxFreeBusyXml || !from.isValid() || !to.isValid() || to <= from) return out; |
| 79 |
1/1✓ Branch 15 → 16 taken 4 times.
|
4 | QDomDocument doc; |
| 80 |
3/3✓ Branch 16 → 17 taken 4 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 3 times.
|
4 | if (!DavXmlHelper::parseInto(doc, xml)) return out; // no DTD or entities; bounded depth |
| 81 |
1/1✓ Branch 22 → 23 taken 3 times.
|
3 | const ElementList responses = findElementsByLocalNameDoc(doc, QStringLiteral("response")); |
| 82 |
2/2✓ Branch 275 → 26 taken 4 times.
✓ Branch 275 → 276 taken 3 times.
|
7 | for (int index = 0; index < responses.size(); ++index) { |
| 83 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 4 times.
|
4 | if (out.size() >= kMaxFreeBusyRecipients) break; |
| 84 |
2/2✓ Branch 29 → 30 taken 4 times.
✓ Branch 30 → 31 taken 4 times.
|
4 | const QDomElement response = responses.at(index).toElement(); |
| 85 |
1/1✓ Branch 35 → 36 taken 4 times.
|
4 | const QDomElement recipient = findFirstElementByLocalName(response, QStringLiteral("recipient")); |
| 86 |
2/3✓ Branch 38 → 39 taken 4 times.
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 4 times.
|
4 | if (recipient.isNull()) continue; // a DAV:multistatus response, not a schedule-response one |
| 87 | 4 | FreeBusy entry; | |
| 88 |
3/3✓ Branch 44 → 45 taken 4 times.
✓ Branch 45 → 46 taken 4 times.
✓ Branch 46 → 47 taken 4 times.
|
4 | entry.recipient = mailtoAddress(findFirstElementByLocalName(recipient, QStringLiteral("href")).text()); |
| 89 |
1/2✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 4 times.
|
4 | if (entry.recipient.isEmpty()) continue; |
| 90 |
3/3✓ Branch 59 → 60 taken 4 times.
✓ Branch 60 → 61 taken 4 times.
✓ Branch 61 → 62 taken 4 times.
|
4 | const QString status = findFirstElementByLocalName(response, QStringLiteral("request-status")).text().trimmed(); |
| 91 |
2/2✓ Branch 69 → 70 taken 4 times.
✓ Branch 70 → 71 taken 4 times.
|
4 | const QString data = findFirstElementByLocalName(response, QStringLiteral("calendar-data")).text(); |
| 92 | // The VFREEBUSY sits inside the VCALENDAR (a bare one is taken too). | ||
| 93 |
1/1✓ Branch 77 → 78 taken 4 times.
|
8 | ICal::Component freebusy = ICal::parseFirst(data, QStringLiteral("VCALENDAR")) |
| 94 |
1/1✓ Branch 81 → 82 taken 4 times.
|
8 | .componentsNamed(QStringLiteral("VFREEBUSY")) |
| 95 |
1/1✓ Branch 82 → 83 taken 4 times.
|
4 | .value(0); |
| 96 |
3/3✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 100 taken 3 times.
✓ Branch 94 → 95 taken 1 time.
|
5 | if (freebusy.name.isEmpty()) freebusy = ICal::parseFirst(data, QStringLiteral("VFREEBUSY")); |
| 97 |
4/5✓ Branch 101 → 102 taken 4 times.
✓ Branch 102 → 103 taken 3 times.
✓ Branch 102 → 107 taken 1 time.
✓ Branch 105 → 106 taken 3 times.
✗ Branch 105 → 107 not taken.
|
4 | entry.known = status.startsWith(QLatin1String("2.")) && freebusy.name == QLatin1String("VFREEBUSY"); |
| 98 |
2/2✓ Branch 108 → 109 taken 3 times.
✓ Branch 108 → 255 taken 1 time.
|
4 | if (entry.known) { |
| 99 | 3 | QList<std::pair<QDateTime, QDateTime>> busy; | |
| 100 |
5/5✓ Branch 112 → 113 taken 3 times.
✓ Branch 115 → 116 taken 3 times.
✓ Branch 116 → 117 taken 3 times.
✓ Branch 227 → 118 taken 5 times.
✓ Branch 227 → 228 taken 3 times.
|
8 | for (const ICal::Property &property : freebusy.propertiesNamed(QStringLiteral("FREEBUSY"))) { |
| 101 |
2/2✓ Branch 122 → 123 taken 5 times.
✓ Branch 123 → 124 taken 5 times.
|
5 | const QString type = property.parameter(QStringLiteral("FBTYPE")).toUpper(); |
| 102 |
5/6✓ Branch 128 → 129 taken 2 times.
✓ Branch 128 → 139 taken 3 times.
✓ Branch 131 → 132 taken 2 times.
✗ Branch 131 → 139 not taken.
✓ Branch 134 → 135 taken 1 time.
✓ Branch 134 → 139 taken 1 time.
|
6 | if (!type.isEmpty() && type != QLatin1String("BUSY") && type != QLatin1String("BUSY-TENTATIVE") && |
| 103 |
3/4✓ Branch 137 → 138 taken 1 time.
✗ Branch 137 → 139 not taken.
✓ Branch 140 → 141 taken 1 time.
✓ Branch 140 → 142 taken 4 times.
|
6 | type != QLatin1String("BUSY-UNAVAILABLE")) |
| 104 | 1 | continue; | |
| 105 |
5/5✓ Branch 145 → 146 taken 4 times.
✓ Branch 146 → 147 taken 4 times.
✓ Branch 147 → 148 taken 4 times.
✓ Branch 217 → 149 taken 5 times.
✓ Branch 217 → 218 taken 4 times.
|
9 | for (const QString &period : property.value.split(QLatin1Char(','))) { |
| 106 |
1/2✗ Branch 151 → 152 not taken.
✓ Branch 151 → 153 taken 5 times.
|
5 | if (busy.size() >= kMaxBusyIntervals) break; |
| 107 | 5 | const qsizetype slash = period.indexOf(QLatin1Char('/')); | |
| 108 |
1/2✗ Branch 156 → 157 not taken.
✓ Branch 156 → 158 taken 5 times.
|
5 | if (slash <= 0) continue; |
| 109 |
2/2✓ Branch 158 → 159 taken 5 times.
✓ Branch 159 → 160 taken 5 times.
|
5 | const QDateTime start = utcValue(period.left(slash)); |
| 110 |
2/2✓ Branch 161 → 162 taken 5 times.
✓ Branch 162 → 163 taken 5 times.
|
5 | QDateTime end = utcValue(period.mid(slash + 1)); |
| 111 | 5 | qint64 seconds = 0; | |
| 112 |
14/20✓ Branch 164 → 165 taken 5 times.
✓ Branch 165 → 166 taken 1 time.
✓ Branch 165 → 172 taken 4 times.
✓ Branch 166 → 167 taken 1 time.
✓ Branch 167 → 168 taken 1 time.
✓ Branch 168 → 169 taken 1 time.
✓ Branch 169 → 170 taken 1 time.
✗ Branch 169 → 172 not taken.
✓ Branch 170 → 171 taken 1 time.
✗ Branch 170 → 172 not taken.
✓ Branch 173 → 174 taken 1 time.
✓ Branch 173 → 175 taken 4 times.
✓ Branch 175 → 176 taken 1 time.
✓ Branch 175 → 177 taken 4 times.
✓ Branch 177 → 178 taken 1 time.
✓ Branch 177 → 183 taken 4 times.
✗ Branch 381 → 382 not taken.
✗ Branch 381 → 383 not taken.
✗ Branch 385 → 386 not taken.
✗ Branch 385 → 387 not taken.
|
5 | if (!end.isValid() && ICalEventCodec::parseDuration(period.mid(slash + 1).trimmed(), &seconds) && seconds > 0) |
| 113 |
1/1✓ Branch 179 → 180 taken 1 time.
|
1 | end = start.addSecs(qMin<qint64>(seconds, 366LL * 24 * 3600)); |
| 114 |
11/17✓ Branch 183 → 184 taken 5 times.
✓ Branch 184 → 185 taken 5 times.
✗ Branch 184 → 196 not taken.
✓ Branch 185 → 186 taken 5 times.
✓ Branch 186 → 187 taken 5 times.
✗ Branch 186 → 196 not taken.
✓ Branch 187 → 188 taken 5 times.
✓ Branch 189 → 190 taken 5 times.
✗ Branch 189 → 196 not taken.
✓ Branch 190 → 191 taken 5 times.
✓ Branch 192 → 193 taken 5 times.
✗ Branch 192 → 196 not taken.
✓ Branch 193 → 194 taken 5 times.
✗ Branch 195 → 196 not taken.
✓ Branch 195 → 197 taken 5 times.
✗ Branch 198 → 199 not taken.
✓ Branch 198 → 200 taken 5 times.
|
5 | if (!start.isValid() || !end.isValid() || end <= start || end <= from || start >= to) continue; |
| 115 |
3/3✓ Branch 200 → 201 taken 5 times.
✓ Branch 201 → 202 taken 5 times.
✓ Branch 203 → 204 taken 5 times.
|
5 | busy.append({qMax(start, from), qMin(end, to)}); |
| 116 | 9 | } | |
| 117 | 8 | } | |
| 118 |
3/3✓ Branch 229 → 230 taken 3 times.
✓ Branch 230 → 231 taken 3 times.
✓ Branch 231 → 232 taken 3 times.
|
3 | std::sort(busy.begin(), busy.end()); |
| 119 |
4/4✓ Branch 232 → 233 taken 3 times.
✓ Branch 233 → 234 taken 3 times.
✓ Branch 252 → 235 taken 5 times.
✓ Branch 252 → 253 taken 3 times.
|
8 | for (const auto &interval : busy) { |
| 120 |
8/8✓ Branch 237 → 238 taken 2 times.
✓ Branch 237 → 243 taken 3 times.
✓ Branch 238 → 239 taken 2 times.
✓ Branch 239 → 240 taken 2 times.
✓ Branch 241 → 242 taken 1 time.
✓ Branch 241 → 243 taken 1 time.
✓ Branch 244 → 245 taken 1 time.
✓ Branch 244 → 249 taken 4 times.
|
5 | if (!entry.busy.isEmpty() && interval.first <= entry.busy.last().second) |
| 121 |
3/3✓ Branch 245 → 246 taken 1 time.
✓ Branch 246 → 247 taken 1 time.
✓ Branch 247 → 248 taken 1 time.
|
1 | entry.busy.last().second = qMax(entry.busy.last().second, interval.second); |
| 122 | else | ||
| 123 |
1/1✓ Branch 249 → 250 taken 4 times.
|
4 | entry.busy.append(interval); |
| 124 | } | ||
| 125 | 3 | } | |
| 126 |
1/1✓ Branch 255 → 256 taken 4 times.
|
4 | out.append(entry); |
| 127 | 4 | } | |
| 128 | 3 | return out; | |
| 129 | 4 | } | |
| 130 | |||
| 131 | } // namespace CalDavXml | ||
| 132 | |||
| 133 |