• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PowerDNS / pdns / 18709198868

22 Oct 2025 07:49AM UTC coverage: 73.016% (+0.01%) from 73.006%
18709198868

Pull #16338

github

web-flow
Merge 53c598698 into 8eda540cb
Pull Request #16338: rec: tighten delegation accept

38301 of 63162 branches covered (60.64%)

Branch coverage included in aggregate %.

30 of 35 new or added lines in 2 files covered. (85.71%)

31 existing lines in 8 files now uncovered.

127486 of 163895 relevant lines covered (77.79%)

6129150.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.87
/pdns/rcpgenerator.cc
1
/*
2
 * This file is part of PowerDNS or dnsdist.
3
 * Copyright -- PowerDNS.COM B.V. and its contributors
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of version 2 of the GNU General Public License as
7
 * published by the Free Software Foundation.
8
 *
9
 * In addition, for the avoidance of any doubt, permission is granted to
10
 * link this program with OpenSSL and to (re)distribute the binaries
11
 * produced as the result of such linking.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
 */
22
#ifdef HAVE_CONFIG_H
23
#include "config.h"
24
#endif
25
#include "rcpgenerator.hh"
26
#include "dnsparser.hh"
27
#include "misc.hh"
28
#include "utility.hh"
29
#include <boost/algorithm/string.hpp>
30
#include <boost/algorithm/string/classification.hpp>
31
#include <boost/algorithm/string/replace.hpp>
32
#include <boost/format.hpp>
33

34
#include <iostream>
35
#include "base32.hh"
36
#include "base64.hh"
37
#include "namespaces.hh"
38

39
RecordTextReader::RecordTextReader(string str, ZoneName zone) :
40
  d_string(std::move(str)), d_zone(std::move(zone))
41
{
2,022,785✔
42
   /* remove whitespace */
43
   if(!d_string.empty() && ( dns_isspace(*d_string.begin()) || dns_isspace(*d_string.rbegin()) ))
2,022,785✔
44
     boost::trim_if(d_string, dns_isspace);
3✔
45
   d_end = d_string.size();
2,022,785✔
46
}
2,022,785✔
47

48
void RecordTextReader::xfr48BitInt(uint64_t &val)
49
{
6✔
50
  auto oldpos = d_pos;
6✔
51
  xfr64BitInt(val);
6✔
52
  if (val >= (1ULL << 48)) {
6!
53
    throw RecordTextException("Numerical value " + d_string.substr(oldpos, d_pos - oldpos) + " at position " + std::to_string(oldpos) + " is too large for a 48-bit integer");
×
54
  }
×
55
}
6✔
56

57
void RecordTextReader::xfrNodeOrLocatorID(NodeOrLocatorID& val) {
12✔
58
  skipSpaces();
12✔
59
  size_t len;
12✔
60
  for(len=0;
12✔
61
      d_pos+len < d_string.length() && (isxdigit(d_string.at(d_pos+len)) || d_string.at(d_pos+len) == ':');
240!
62
      len++) ;   // find length of ID
228✔
63

64
  // Parse as v6, and then strip the final 64 zero bytes
65
  struct in6_addr tmpbuf;
12✔
66
  string to_parse = d_string.substr(d_pos, len) + ":0:0:0:0";
12✔
67

68
  if (inet_pton(AF_INET6, to_parse.c_str(), &tmpbuf) != 1) {
12!
69
    throw RecordTextException("while parsing colon-delimited 64-bit field: '" + d_string.substr(d_pos, len) + "' is invalid");
×
70
  }
×
71

72
  std::memcpy(&val.content, tmpbuf.s6_addr, sizeof(val.content));
12✔
73
  d_pos += len;
12✔
74
}
12✔
75

76
void RecordTextReader::xfr64BitInt(uint64_t &val)
77
{
626,000✔
78
  skipSpaces();
626,000✔
79

80
  if(!isdigit(d_string.at(d_pos)))
626,000!
81
    throw RecordTextException("expected digits at position "+std::to_string(d_pos)+" in '"+d_string+"'");
×
82

83
  size_t pos;
626,000✔
84
  val=std::stoull(d_string.substr(d_pos), &pos);
626,000✔
85

86
  d_pos += pos;
626,000✔
87
}
626,000✔
88

89

90
void RecordTextReader::xfr32BitInt(uint32_t &val)
91
{
2,363,260✔
92
  skipSpaces();
2,363,260✔
93

94
  if(!isdigit(d_string.at(d_pos)))
2,363,260!
95
    throw RecordTextException("expected digits at position "+std::to_string(d_pos)+" in '"+d_string+"'");
×
96

97
  size_t pos;
2,363,260✔
98
  val = pdns::checked_stoi<uint32_t>(d_string.c_str() + d_pos, &pos);
2,363,260✔
99

100
  d_pos += pos;
2,363,260✔
101
}
2,363,260✔
102

103
void RecordTextReader::xfrTime(uint32_t &val)
104
{
625,994✔
105
  struct tm tm;
625,994✔
106
  memset(&tm, 0, sizeof(tm));
625,994✔
107

108
  uint64_t itmp;
625,994✔
109
  xfr64BitInt(itmp);
625,994✔
110

111
  if (itmp <= (uint32_t)~0) {
625,994✔
112
    // formatted as seconds since epoch, not as YYYYMMDDHHmmSS:
113
    val = (uint32_t) itmp;
6✔
114
    return;
6✔
115
  }
6✔
116

117
  ostringstream tmp;
625,988✔
118

119
  tmp<<itmp;
625,988✔
120

121
  if (sscanf(tmp.str().c_str(), "%04d%02d%02d" "%02d%02d%02d",
625,988!
122
             &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
625,988✔
123
             &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
625,988✔
124
    throw RecordTextException("unable to parse '"+std::to_string(itmp)+"' into a valid time at position "+std::to_string(d_pos)+" in '"+d_string+"'");
×
125
  }
×
126

127
  tm.tm_year-=1900;
625,988✔
128
  tm.tm_mon-=1;
625,988✔
129
  // coverity[store_truncates_time_t]
130
  val=(uint32_t)Utility::timegm(&tm);
625,988✔
131
}
625,988✔
132

133
void RecordTextReader::xfrIP(uint32_t &val)
134
{
1,291,935✔
135
  skipSpaces();
1,291,935✔
136

137
  if(!isdigit(d_string.at(d_pos)))
1,291,935✔
138
    throw RecordTextException("while parsing IP address, expected digits at position "+std::to_string(d_pos)+" in '"+d_string+"'");
4✔
139

140
  uint32_t octet=0;
1,291,931✔
141
  val=0;
1,291,931✔
142
  char count=0;
1,291,931✔
143
  bool last_was_digit = false;
1,291,931✔
144

145
  for(;;) {
16,177,028✔
146
    if(d_string.at(d_pos)=='.') {
16,177,028✔
147
      if (!last_was_digit)
3,875,769✔
148
        throw RecordTextException(string("unable to parse IP address, dot without previous digit"));
3✔
149
      last_was_digit = false;
3,875,766✔
150
      val<<=8;
3,875,766✔
151
      val+=octet;
3,875,766✔
152
      octet=0;
3,875,766✔
153
      count++;
3,875,766✔
154
      if(count > 3)
3,875,766✔
155
        throw RecordTextException(string("unable to parse IP address, too many dots"));
3✔
156
    }
3,875,766✔
157
    else if(isdigit(d_string.at(d_pos))) {
12,301,259✔
158
      last_was_digit = true;
12,301,253✔
159
      octet*=10;
12,301,253✔
160
      octet+=d_string.at(d_pos) - '0';
12,301,253✔
161
      if(octet > 255)
12,301,253✔
162
        throw RecordTextException("unable to parse IP address");
6✔
163
    }
12,301,253✔
164
    else if(dns_isspace(d_string.at(d_pos)) || d_string.at(d_pos) == ',')
6!
165
      break;
6✔
UNCOV
166
    else {
×
UNCOV
167
      throw RecordTextException(string("unable to parse IP address, strange character: ")+d_string.at(d_pos));
×
UNCOV
168
    }
×
169
    d_pos++;
16,177,010✔
170
    if(d_pos == d_string.length())
16,177,010✔
171
      break;
1,291,913✔
172
  }
16,177,010✔
173
  if (count != 3)
1,291,919✔
174
    throw RecordTextException(string("unable to parse IP address, not enough dots"));
3✔
175
  if (!last_was_digit)
1,291,916✔
176
    throw RecordTextException(string("unable to parse IP address, trailing dot"));
3✔
177
  val<<=8;
1,291,913✔
178
  val+=octet;
1,291,913✔
179
  val=ntohl(val);
1,291,913✔
180
}
1,291,913✔
181

182

183
void RecordTextReader::xfrIP6(std::string &val)
184
{
6,290✔
185
  struct in6_addr tmpbuf;
6,290✔
186

187
  skipSpaces();
6,290✔
188

189
  size_t len;
6,290✔
190
  // lookup end of value - think of ::ffff encoding too, has dots in it!
191
  for(len=0;
6,290✔
192
      d_pos+len < d_string.length() && (isxdigit(d_string.at(d_pos+len)) || d_string.at(d_pos+len) == ':' || d_string.at(d_pos+len)=='.');
106,416✔
193
    len++);
100,126✔
194

195
  if(!len)
6,290!
196
    throw RecordTextException("while parsing IPv6 address, expected xdigits at position "+std::to_string(d_pos)+" in '"+d_string+"'");
×
197

198
  // end of value is here, try parse as IPv6
199
  string address=d_string.substr(d_pos, len);
6,290✔
200

201
  if (inet_pton(AF_INET6, address.c_str(), &tmpbuf) != 1) {
6,290✔
202
    throw RecordTextException("while parsing IPv6 address: '" + address + "' is invalid");
9✔
203
  }
9✔
204

205
  val = std::string((char*)tmpbuf.s6_addr, 16);
6,281✔
206

207
  d_pos += len;
6,281✔
208
}
6,281✔
209

210
void RecordTextReader::xfrCAWithoutPort(uint8_t version, ComboAddress &val)
211
{
×
212
  if (version == 4) {
×
213
    uint32_t ip;
×
214
    xfrIP(ip);
×
215
    val = makeComboAddressFromRaw(4, string((const char*) &ip, 4));
×
216
  }
×
217
  else if (version == 6) {
×
218
    string ip;
×
219
    xfrIP6(ip);
×
220
    val = makeComboAddressFromRaw(6, ip);
×
221
  }
×
222
  else throw RecordTextException("invalid address family");
×
223
}
×
224

225
void RecordTextReader::xfrCAPort(ComboAddress &val)
226
{
×
227
  uint16_t port;
×
228
  xfr16BitInt(port);
×
229
  val.sin4.sin_port = port;
×
230
}
×
231

232
bool RecordTextReader::eof()
233
{
2,595,250✔
234
  return d_pos==d_end;
2,595,250✔
235
}
2,595,250✔
236

237
void RecordTextReader::xfr16BitInt(uint16_t &val)
238
{
656,479✔
239
  auto oldpos = d_pos;
656,479✔
240
  uint32_t tmp{0};
656,479✔
241
  xfr32BitInt(tmp);
656,479✔
242
  val=tmp;
656,479✔
243
  if(val!=tmp) {
656,479!
244
    throw RecordTextException("Numerical value " + d_string.substr(oldpos, d_pos - oldpos) + " at position " + std::to_string(oldpos) + " is too large for a 16-bit integer");
×
245
  }
×
246
}
656,479✔
247

248
void RecordTextReader::xfr8BitInt(uint8_t &val)
249
{
1,288,107✔
250
  auto oldpos = d_pos;
1,288,107✔
251
  uint32_t tmp{0};
1,288,107✔
252
  xfr32BitInt(tmp);
1,288,107✔
253
  val=tmp;
1,288,107✔
254
  if(val!=tmp) {
1,288,107!
255
    throw RecordTextException("Numerical value " + d_string.substr(oldpos, d_pos - oldpos) + " at position " + std::to_string(oldpos) + " is too large for a 8-bit integer");
×
256
  }
×
257
}
1,288,107✔
258

259
// this code should leave all the escapes around
260
void RecordTextReader::xfrName(DNSName& val, [[maybe_unused]] bool compress)
261
{
403,476✔
262
  skipSpaces();
403,476✔
263
  DNSName sval;
403,476✔
264

265
  string::size_type begin_pos = d_pos;
403,476✔
266
  while (d_pos < d_end) {
4,768,038✔
267
    if (d_string[d_pos]!='\r' && dns_isspace(d_string[d_pos])) {
4,722,543✔
268
      break;
357,981✔
269
    }
357,981✔
270

271
    d_pos++;
4,364,562✔
272
  }
4,364,562✔
273

274
  {
403,476✔
275
    std::string_view view(d_string);
403,476✔
276
    sval = DNSName(view.substr(begin_pos, d_pos - begin_pos));
403,476✔
277
  }
403,476✔
278

279
  if (sval.empty()) {
403,476!
280
    sval = DNSName(d_zone);
×
281
  }
×
282
  else if (!d_zone.empty()) {
403,476✔
283
    sval += DNSName(d_zone);
394,590✔
284
  }
394,590✔
285
  val = std::move(sval);
403,476✔
286
}
403,476✔
287

288
static bool isbase64(char c, bool acceptspace)
289
{
23,940,891✔
290
  if(dns_isspace(c))
23,940,891✔
291
    return acceptspace;
181✔
292
  if(c >= '0' && c <= '9')
23,940,710✔
293
    return true;
3,584,966✔
294
  if(c >= 'a' && c <= 'z')
20,355,744!
295
    return true;
9,670,543✔
296
  if(c >= 'A' && c <= 'Z')
10,685,201!
297
    return true;
9,449,413✔
298
  if(c=='+' || c=='/' || c=='=')
1,235,788✔
299
    return true;
1,235,786✔
300
  return false;
2,147,483,668✔
301
}
1,235,788✔
302

303
void RecordTextReader::xfrBlobNoSpaces(string& val, int len)
304
{
39✔
305
  skipSpaces();
39✔
306
  auto pos = d_pos;
39✔
307
  const char* strptr=d_string.c_str();
39✔
308
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
309
  while(d_pos < d_end && isbase64(strptr[d_pos], false)) {
951✔
310
    d_pos++;
912✔
311
  }
912✔
312

313
  string tmp;
39✔
314
  tmp.assign(d_string.c_str()+pos, d_string.c_str() + d_pos);
39✔
315
  boost::erase_all(tmp," ");
39✔
316
  val.clear();
39✔
317
  B64Decode(tmp, val);
39✔
318

319
  if (len>-1 && val.size() != static_cast<size_t>(len))
39!
320
    throw RecordTextException("Record length "+std::to_string(val.size()) + " does not match expected length '"+std::to_string(len));
×
321
}
39✔
322

323
void RecordTextReader::xfrBlob(string& val, int)
324
{
314,330✔
325
  skipSpaces();
314,330✔
326
  auto pos = d_pos;
314,330✔
327
  const char* strptr=d_string.c_str();
314,330✔
328
  // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
329
  while(d_pos < d_end && isbase64(strptr[d_pos], true)) {
24,254,277✔
330
    d_pos++;
23,939,947✔
331
  }
23,939,947✔
332

333
  string tmp;
314,330✔
334
  tmp.assign(d_string.c_str()+pos, d_string.c_str() + d_pos);
314,330✔
335
  boost::erase_all(tmp," ");
314,330✔
336
  val.clear();
314,330✔
337
  B64Decode(tmp, val);
314,330✔
338
}
314,330✔
339

340
void RecordTextReader::xfrRFC1035CharString(string &val) {
1,109✔
341
  auto ctr = parseRFC1035CharString(d_string.substr(d_pos, d_end - d_pos), val);
1,109✔
342
  d_pos += ctr;
1,109✔
343
}
1,109✔
344

345
void RecordTextReader::xfrSVCBValueList(vector<string> &val) {
688✔
346
  auto ctr = parseSVCBValueList(d_string.substr(d_pos, d_end - d_pos), val);
688✔
347
  d_pos += ctr;
688✔
348
}
688✔
349

350
void RecordTextReader::xfrSvcParamKeyVals(set<SvcParam>& val) // NOLINT(readability-function-cognitive-complexity)
351
{
1,098✔
352
  while (d_pos != d_end) {
3,110✔
353
    skipSpaces();
2,153✔
354
    if (d_pos == d_end)
2,153!
355
      return;
×
356

357
    // Find the SvcParamKey
358
    auto pos = d_pos;
2,153✔
359
    while (d_pos != d_end) {
16,595✔
360
      if (d_string.at(d_pos) == '=' || d_string.at(d_pos) == ' ') {
16,541✔
361
        break;
2,099✔
362
      }
2,099✔
363
      d_pos++;
14,442✔
364
    }
14,442✔
365

366
    // We've reached a space or equals-sign or the end of the string (d_pos is at this char)
367
    string k = d_string.substr(pos, d_pos - pos);
2,153✔
368
    SvcParam::SvcParamKey key;
2,153✔
369
    bool generic;
2,153✔
370
    try {
2,153✔
371
      key = SvcParam::keyFromString(k, generic);
2,153✔
372
    } catch (const std::invalid_argument &e) {
2,153✔
373
      throw RecordTextException(e.what());
3✔
374
    }
3✔
375

376
    if (d_pos != d_end && d_string.at(d_pos) == '=') {
2,150✔
377
      d_pos++; // Now on the first character after '='
1,854✔
378
      if (d_pos == d_end || d_string.at(d_pos) == ' ') {
1,854✔
379
        throw RecordTextException("expected value after " + k + "=");
51✔
380
      }
51✔
381
    }
1,854✔
382

383
    switch (key) {
2,099✔
384
    case SvcParam::no_default_alpn:
139✔
385
    case SvcParam::ohttp:
278✔
386
      if (d_pos != d_end && d_string.at(d_pos) != ' ') {
278✔
387
        throw RecordTextException(k + " key can not have values");
12✔
388
      }
12✔
389
      val.insert(SvcParam(key));
266✔
390
      break;
266✔
391
    case SvcParam::ipv4hint: /* fall-through */
87✔
392
    case SvcParam::ipv6hint: {
144✔
393
      vector<ComboAddress> hints;
144✔
394
      bool doAuto{false};
144✔
395
      if (generic) {
144✔
396
        string value;
18✔
397
        xfrRFC1035CharString(value);
18✔
398
        size_t len = key == SvcParam::ipv4hint ? 4 : 16;
18✔
399
        if (value.size() % len != 0) {
18✔
400
          throw RecordTextException(k + " in generic format has wrong number of bytes");
6✔
401
        }
6✔
402
        for (size_t i=0; i<value.size(); i += len) {
30✔
403
          auto hint = makeComboAddressFromRaw(static_cast<uint8_t>(key), &value.at(i), len);
18✔
404
          hints.push_back(hint);
18✔
405
        }
18✔
406
      } else {
126✔
407
        vector<string> value;
126✔
408
        xfrSVCBValueList(value);
126✔
409
        for (auto const &v: value) {
165✔
410
          if (v == "auto") {
165✔
411
            doAuto = true;
35✔
412
            hints.clear();
35✔
413
            break;
35✔
414
          }
35✔
415
          hints.push_back(ComboAddress(v));
130✔
416
        }
130✔
417
      }
126✔
418
      if (!doAuto && hints.empty()) {
138!
419
        throw RecordTextException("value is required for SVC Param " + k);
×
420
      }
×
421
      try {
138✔
422
        auto p = SvcParam(key, std::move(hints));
138✔
423
        p.setAutoHint(doAuto);
138✔
424
        val.insert(std::move(p));
138✔
425
      }
138✔
426
      catch (const std::invalid_argument& e) {
138✔
427
        throw RecordTextException(e.what());
12✔
428
      }
12✔
429
      break;
126✔
430
    }
138✔
431
    case SvcParam::alpn: {
538✔
432
      vector<string> value;
538✔
433
      if (generic) {
538✔
434
        string v;
15✔
435
        xfrRFC1035CharString(v);
15✔
436
        size_t spos{0}, len;
15✔
437
        while (spos < v.length()) {
30✔
438
          len = v.at(spos);
24✔
439
          spos += 1;
24✔
440
          if (len == 0) {
24✔
441
            throw RecordTextException("ALPN values cannot be empty strings");
3✔
442
          }
3✔
443
          if (len > v.length() - spos) {
21✔
444
            throw RecordTextException("Length of ALPN value goes over total length of alpn SVC Param");
6✔
445
          }
6✔
446
          value.push_back(v.substr(spos, len));
15✔
447
          spos += len;
15✔
448
        }
15✔
449
      } else {
523✔
450
        xfrSVCBValueList(value);
523✔
451
      }
523✔
452
      if (value.empty()) {
529✔
453
        throw RecordTextException("value is required for SVC Param " + k);
3✔
454
      }
3✔
455
      for (const auto &alpn_value : value) {
689✔
456
        if (alpn_value.empty()) {
689!
457
          throw RecordTextException("ALPN values cannot be empty strings");
×
458
        }
×
459
      }
689✔
460
      val.insert(SvcParam(key, std::move(value)));
526✔
461
      break;
526✔
462
    }
526✔
463
    case SvcParam::mandatory: {
48✔
464
      if (generic) {
48✔
465
        string v;
9✔
466
        xfrRFC1035CharString(v);
9✔
467
        if (v.empty()) {
9✔
468
          throw RecordTextException("value is required for SVC Param " + k);
3✔
469
        }
3✔
470
        if (v.length() % 2 != 0) {
6✔
471
          throw RecordTextException("Wrong number of bytes in SVC Param " + k);
3✔
472
        }
3✔
473
        std::set<SvcParam::SvcParamKey> keys;
3✔
474
        for (size_t i=0; i < v.length(); i += 2) {
9✔
475
          uint16_t mand = (v.at(i) << 8);
6✔
476
          mand += v.at(i+1);
6✔
477
          keys.insert(SvcParam::SvcParamKey(mand));
6✔
478
        }
6✔
479
        val.insert(SvcParam(key, std::move(keys)));
3✔
480
        break;
3✔
481
      }
6✔
482
      vector<string> parts;
39✔
483
      xfrSVCBValueList(parts);
39✔
484
      if (parts.empty()) {
39!
485
        throw RecordTextException("value is required for SVC Param " + k);
×
486
      }
×
487
      set<string> values(parts.begin(), parts.end());
39✔
488
      val.insert(SvcParam(key, std::move(values)));
39✔
489
      break;
39✔
490
    }
39✔
491
    case SvcParam::port: {
381✔
492
      uint16_t port;
381✔
493
      if (generic) {
381✔
494
        string v;
6✔
495
        xfrRFC1035CharString(v);
6✔
496
        if (v.length() != 2) {
6✔
497
          throw RecordTextException("port in generic format has the wrong length, expected 2, got " + std::to_string(v.length()));
3✔
498
        }
3✔
499
        port = (v.at(0) << 8);
3✔
500
        port += v.at(1);
3✔
501
      } else {
375✔
502
        string portstring;
375✔
503
        xfrRFC1035CharString(portstring);
375✔
504
        if (portstring.empty()) {
375✔
505
          throw RecordTextException("value is required for SVC Param " + k);
3✔
506
        }
3✔
507
        try {
372✔
508
          pdns::checked_stoi_into(port, portstring);
372✔
509
        } catch (const std::exception &e) {
372✔
510
          throw RecordTextException(e.what());
6✔
511
        }
6✔
512
      }
372✔
513
      val.insert(SvcParam(key, port));
369✔
514
      break;
369✔
515
    }
381✔
516
    case SvcParam::ech: {
30✔
517
      string value;
30✔
518
      if (generic) {
30✔
519
        xfrRFC1035CharString(value);
6✔
520
      } else {
24✔
521
        bool haveQuote = d_string.at(d_pos) == '"';
24✔
522
        if (haveQuote) {
24✔
523
          d_pos++;
21✔
524
        }
21✔
525
        xfrBlobNoSpaces(value);
24✔
526
        if (haveQuote) {
24✔
527
          if (d_string.at(d_pos) != '"') {
21!
528
            throw RecordTextException("ech value starts, but does not end with a '\"' symbol");
×
529
          }
×
530
          d_pos++;
21✔
531
        }
21✔
532
      }
24✔
533
      if (value.empty()) {
30✔
534
        throw RecordTextException("value is required for SVC Param " + k);
6✔
535
      }
6✔
536
      val.insert(SvcParam(key, value));
24✔
537
      break;
24✔
538
    }
30✔
539
    case SvcParam::tls_supported_groups: {
142✔
540
      string string_value;
142✔
541
      xfrRFC1035CharString(string_value);
142✔
542
      if (string_value.empty()) {
142✔
543
        throw RecordTextException("Value is required for SVC Param " + k);
6✔
544
      }
6✔
545

546
      vector<string> parts;
136✔
547
      stringtok(parts, string_value, ",");
136✔
548

549
      vector<uint16_t> values;
136✔
550
      values.reserve(parts.size());
136✔
551
      for (const auto& part : parts) {
266✔
552
        uint16_t int_part{0};
266✔
553
        try {
266✔
554
          pdns::checked_stoi_into(int_part, part);
266✔
555
        } catch (const std::invalid_argument&) {
266✔
556
          throw RecordTextException("Value in invalid format for SVC Param " + k);
6✔
557
        }
6✔
558
        values.emplace_back(int_part);
260✔
559
      }
260✔
560

561
      val.insert(SvcParam(key, std::move(values)));
130✔
562
      break;
130✔
563
    }
136✔
564
    case SvcParam::dohpath: {
139✔
565
      string value;
139✔
566
      xfrRFC1035CharString(value);
139✔
567
      if (value.empty()) {
139✔
568
        throw RecordTextException("Value is required for SVC Param " + k);
6✔
569
      }
6✔
570
      val.insert(SvcParam(key, value));
133✔
571
      break;
133✔
572
    }
139✔
573
    default: {
399✔
574
      string value;
399✔
575
      xfrRFC1035CharString(value);
399✔
576
      if (!generic && value.empty()) {
399!
577
        // for generic format, we do not know.
578
        // Known keys which forbid having a value need to implement a switch case, above.
579
        throw RecordTextException("value is required for SVC Param " + k);
×
580
      }
×
581
      val.insert(SvcParam(key, value));
399✔
582
      break;
399✔
583
    }
399✔
584
    }
2,099✔
585
  }
2,099✔
586
}
1,098✔
587

588
static inline uint8_t hextodec(uint8_t val)
589
{
1,706,230✔
590
  if(val >= '0' && val<='9')
1,706,230!
591
    return val-'0';
331,572✔
592
  else if(val >= 'A' && val<='F')
1,374,659✔
593
    return 10+(val-'A');
9,166✔
594
  else if(val >= 'a' && val<='f')
1,365,493✔
595
    return 10+(val-'a');
1,365,494✔
596
  else
4,294,967,294✔
597
    throw RecordTextException("Unknown hexadecimal character '"+std::to_string(val)+"'");
4,294,967,294✔
598
}
1,706,230✔
599

600

601
static void HEXDecode(std::string_view chunk, string& out)
602
{
329,688✔
603
  out.clear();
329,688✔
604
  if (chunk.length() == 1 && chunk[0] == '-') {
329,688!
605
    return;
35✔
606
  }
35✔
607
  out.reserve(chunk.length() / 2);
329,653✔
608
  bool lowdigit{false};
329,653✔
609
  uint8_t val{0};
329,653✔
610
  for (auto chr : chunk) {
1,706,403✔
611
    if(isalnum(chr) == 0) {
1,706,403✔
612
      continue;
170✔
613
    }
170✔
614
    if (!lowdigit) {
1,706,233✔
615
      val = 16*hextodec(chr);
853,118✔
616
      lowdigit = true;
853,118✔
617
    } else {
853,120✔
618
      val += hextodec(chr);
853,115✔
619
      out.append(1, (char) val);
853,115✔
620
      lowdigit = false;
853,115✔
621
      val = 0;
853,115✔
622
    }
853,115✔
623
  }
1,706,233✔
624
  if (lowdigit) {
329,653✔
625
    throw RecordTextException("Hexadecimal blob '" + std::string(chunk) + "' contains an odd number of hex digits");
3✔
626
  }
3✔
627
}
329,653✔
628

629
void RecordTextReader::xfrHexBlob(string& val, bool keepReading)
630
{
329,688✔
631
  skipSpaces();
329,688✔
632
  auto pos = d_pos;
329,688✔
633
  while(d_pos < d_end && (keepReading || !dns_isspace(d_string[d_pos]))) {
2,036,124✔
634
    d_pos++;
1,706,436✔
635
  }
1,706,436✔
636

637
  HEXDecode(std::string_view(d_string).substr(pos, d_pos - pos), val);
329,688✔
638
}
329,688✔
639

640
void RecordTextReader::xfrBase32HexBlob(string& val)
641
{
282,589✔
642
  skipSpaces();
282,589✔
643
  auto pos = d_pos;
282,589✔
644
  while(d_pos < d_end && !dns_isspace(d_string[d_pos])) {
8,365,293✔
645
    d_pos++;
8,082,704✔
646
  }
8,082,704✔
647

648
  val=fromBase32Hex(string(d_string.c_str()+pos, d_pos-pos));
282,589✔
649
}
282,589✔
650

651

652
void RecordTextWriter::xfrBase32HexBlob(const string& val)
653
{
261,870✔
654
  if(!d_string.empty())
261,870!
655
    d_string.append(1,' ');
261,870✔
656

657
  d_string.append(toUpper(toBase32Hex(val)));
261,870✔
658
}
261,870✔
659

660

661
void RecordTextReader::xfrText(string& val, bool multi, bool /* lenField */)
662
{
11,210✔
663
  val.clear();
11,210✔
664
  val.reserve(d_end - d_pos);
11,210✔
665

666
  while (d_pos != d_end) {
18,646✔
667
    if (!val.empty()) {
12,564✔
668
      val.append(1, ' ');
1,356✔
669
    }
1,356✔
670

671
    skipSpaces();
12,564✔
672
    char delimiter{'"'};
12,564✔
673
    bool quoted = d_string[d_pos] == '"';
12,564✔
674
    // If the word is quoted, process up to the next quote; otherwise,
675
    // process up to the next whitespace (but output it in quotes).
676
    val.append(1, '"');
12,564✔
677
    if (quoted) {
12,564✔
678
      ++d_pos;
12,493✔
679
    }
12,493✔
680
    else {
71✔
681
      // RFC1035: ``a contiguous set of characters without interior spaces''
682
      delimiter = ' ';
71✔
683
    }
71✔
684
    while (d_pos != d_end && d_string[d_pos] != delimiter) {
425,145✔
685
      if (d_string[d_pos] == '\\' && d_pos + 1 != d_end) {
412,589!
686
        val.append(1, d_string[d_pos++]); // copy escape slash
2,078✔
687
        char chr = d_string[d_pos];
2,078✔
688
        if (chr >= '0' && chr <= '9') {
2,078✔
689
          bool valid{false};
1,274✔
690
          // Must be a three-digit character escape sequence
691
          if (d_end - d_pos >= 3) {
1,274!
692
            char chr2 = d_string[d_pos + 1];
1,274✔
693
            char chr3 = d_string[d_pos + 2];
1,274✔
694
            if (chr2 >= '0' && chr2 <= '9' && chr3 >= '0' && chr3 <= '9') {
1,274!
695
              valid = 100 * (chr - '0') + 10 * (chr2 - '0') + chr3 - '0' < 256;
1,271✔
696
            }
1,271✔
697
          }
1,274✔
698
          if (!valid) {
1,274✔
699
            throw RecordTextException("Data field in DNS contains an invalid escape at position "+std::to_string(d_pos)+" of '"+d_string+"'");
6✔
700
          }
6✔
701
        }
1,274✔
702
        // Not advancing d_pos, we'll append the next 1 or 3 characters as
703
        // part of the regular case.
704
      }
2,078✔
705
      if (!quoted && d_string[d_pos] == '"') {
412,583✔
706
        // Bind allows a non-quoted text to be immediately followed by a
707
        // quoted text, without any whitespace in between, so handle this
708
        // as a delimiter.
709
        break;
4✔
710
      }
4✔
711
      val.append(1, d_string[d_pos]);
412,579✔
712
      ++d_pos;
412,579✔
713
    }
412,579✔
714
    val.append(1,'"');
12,558✔
715
    if (quoted) {
12,558✔
716
      // If we reached the end in a quoted section, the closing quote is missing.
717
      if (d_pos == d_end) {
12,493✔
718
        throw RecordTextException("Data field in DNS should end on a quote (\") in '"+d_string+"'");
3✔
719
      }
3✔
720
      // Skip closing quote
721
      ++d_pos;
12,490✔
722
    }
12,490✔
723
    if (!multi) {
12,555✔
724
      break;
5,119✔
725
    }
5,119✔
726
  }
12,555✔
727
}
11,210✔
728

729
void RecordTextReader::xfrUnquotedText(string& val, bool /* lenField */)
730
{
21✔
731
  val.clear();
21✔
732
  val.reserve(d_end - d_pos);
21✔
733

734
  if(!val.empty())
21!
735
    val.append(1, ' ');
×
736

737
  skipSpaces();
21✔
738
  val.append(1, d_string[d_pos]);
21✔
739
  while(++d_pos < d_end && d_string[d_pos] != ' '){
105!
740
    val.append(1, d_string[d_pos]);
84✔
741
  }
84✔
742
}
21✔
743

744
void RecordTextReader::xfrType(uint16_t& val)
745
{
925,496✔
746
  skipSpaces();
925,496✔
747
  auto pos = d_pos;
925,496✔
748
  while(d_pos < d_end && !dns_isspace(d_string[d_pos])) {
3,830,808✔
749
    d_pos++;
2,905,312✔
750
  }
2,905,312✔
751

752
  string tmp;
925,496✔
753
  tmp.assign(d_string.c_str()+pos, d_string.c_str() + d_pos);
925,496✔
754

755
  val=DNSRecordContent::TypeToNumber(tmp);
925,496✔
756
}
925,496✔
757

758

759
void RecordTextReader::skipSpaces()
760
{
6,557,855✔
761
  const char* strptr = d_string.c_str();
6,557,855✔
762
  while(d_pos < d_end && dns_isspace(strptr[d_pos]))
11,099,572✔
763
    d_pos++;
4,541,717✔
764
  if(d_pos == d_end)
6,557,855!
765
    throw RecordTextException("missing field at the end of record content '"+d_string+"'");
×
766
}
6,557,855✔
767

768

769
RecordTextWriter::RecordTextWriter(string& str, bool noDot) : d_string(str)
770
{
2,735,711✔
771
  d_string.clear();
2,735,711✔
772
  d_nodot=noDot;
2,735,711✔
773
}
2,735,711✔
774

775
void RecordTextWriter::xfrNodeOrLocatorID(const NodeOrLocatorID& val)
776
{
21✔
777
  if(!d_string.empty()) {
21✔
778
    d_string.append(1,' ');
18✔
779
  }
18✔
780

781
  size_t ctr = 0;
21✔
782
  char tmp[5];
21✔
783
  for (auto const &c : val.content) {
168✔
784
    snprintf(tmp, sizeof(tmp), "%02X", c);
168✔
785
    d_string+=tmp;
168✔
786
    ctr++;
168✔
787
    if (ctr % 2 == 0 && ctr != 8) {
168✔
788
      d_string+=':';
63✔
789
    }
63✔
790
  }
168✔
791
}
21✔
792

793
void RecordTextWriter::xfr48BitInt(const uint64_t& val)
794
{
81✔
795
  if(!d_string.empty())
81!
796
    d_string.append(1,' ');
81✔
797
  d_string+=std::to_string(val);
81✔
798
}
81✔
799

800

801
void RecordTextWriter::xfr32BitInt(const uint32_t& val)
802
{
4,457,831✔
803
  if(!d_string.empty())
4,457,831✔
804
    d_string.append(1,' ');
4,166,438✔
805
  d_string+=std::to_string(val);
4,457,831✔
806
}
4,457,831✔
807

808
void RecordTextWriter::xfrType(const uint16_t& val)
809
{
873,650✔
810
  if(!d_string.empty())
873,650!
811
    d_string.append(1,' ');
×
812
  d_string+=DNSRecordContent::NumberToType(val);
873,650✔
813
}
873,650✔
814

815
// this function is on the fast path for the pdns_recursor
816
void RecordTextWriter::xfrIP(const uint32_t& val)
817
{
1,225,676✔
818
  if(!d_string.empty())
1,225,676✔
819
    d_string.append(1,' ');
18✔
820

821
  char tmp[17];
1,225,676✔
822
  uint32_t ip=val;
1,225,676✔
823
  uint8_t vals[4];
1,225,676✔
824

825
  memcpy(&vals[0], &ip, sizeof(ip));
1,225,676✔
826

827
  char *pos=tmp;
1,225,676✔
828

829
  for(int n=0; n < 4; ++n) {
6,128,380✔
830
    if(vals[n]<10) {
4,902,704✔
831
      *(pos++)=vals[n]+'0';
1,308,158✔
832
    } else if(vals[n] < 100) {
3,598,658✔
833
      *(pos++)=(vals[n]/10) +'0';
487,751✔
834
      *(pos++)=(vals[n]%10) +'0';
487,751✔
835
    } else {
3,106,795✔
836
      *(pos++)=(vals[n]/100) +'0';
3,106,795✔
837
      vals[n]%=100;
3,106,795✔
838
      *(pos++)=(vals[n]/10) +'0';
3,106,795✔
839
      *(pos++)=(vals[n]%10) +'0';
3,106,795✔
840
    }
3,106,795✔
841
    if(n!=3)
4,902,704✔
842
      *(pos++)='.';
3,677,028✔
843
  }
4,902,704✔
844
  *pos=0;
1,225,676✔
845
  d_string.append(tmp, pos);
1,225,676✔
846
}
1,225,676✔
847

848
void RecordTextWriter::xfrIP6(const std::string& val)
849
{
2,613✔
850
  char tmpbuf[16];
2,613✔
851
  char addrbuf[40];
2,613✔
852

853
  if(!d_string.empty())
2,613✔
854
   d_string.append(1,' ');
12✔
855

856
  val.copy(tmpbuf,16);
2,613✔
857

858
  if (inet_ntop(AF_INET6, tmpbuf, addrbuf, sizeof addrbuf) == nullptr)
2,613!
859
    throw RecordTextException("Unable to convert to ipv6 address");
×
860

861
  d_string += std::string(addrbuf);
2,613✔
862
}
2,613✔
863

864
void RecordTextWriter::xfrCAWithoutPort(uint8_t /* version */, ComboAddress &val)
865
{
×
866
  string ip = val.toString();
×
867

868
  if(!d_string.empty())
×
869
    d_string.append(1,' ');
×
870

871
  d_string += ip;
×
872
}
×
873

874
void RecordTextWriter::xfrCAPort(ComboAddress &val)
875
{
×
876
  xfr16BitInt(val.sin4.sin_port);
×
877
}
×
878

879
void RecordTextWriter::xfrTime(const uint32_t& val)
880
{
1,747,301✔
881
  if(!d_string.empty())
1,747,301!
882
    d_string.append(1,' ');
1,747,301✔
883

884
  struct tm tm;
1,747,301✔
885
  time_t time=val; // Y2038 bug!
1,747,301✔
886
  gmtime_r(&time, &tm);
1,747,301✔
887

888
  static const boost::format fmt("%04d%02d%02d" "%02d%02d%02d");
1,747,301✔
889
  d_string += boost::str(boost::format(fmt) % (tm.tm_year+1900) % (tm.tm_mon+1) % tm.tm_mday % tm.tm_hour % tm.tm_min % tm.tm_sec);
1,747,301✔
890
}
1,747,301✔
891

892

893
void RecordTextWriter::xfr16BitInt(const uint16_t& val)
894
{
1,175,767✔
895
  xfr32BitInt(val);
1,175,767✔
896
}
1,175,767✔
897

898
void RecordTextWriter::xfr8BitInt(const uint8_t& val)
899
{
2,291,652✔
900
  xfr32BitInt(val);
2,291,652✔
901
}
2,291,652✔
902

903
// should not mess with the escapes
904
void RecordTextWriter::xfrName(const DNSName& val, bool /* unused */)
905
{
1,100,804✔
906
  if(!d_string.empty())
1,100,804✔
907
    d_string.append(1,' ');
915,498✔
908

909
  if(d_nodot) {
1,100,804✔
910
    d_string+=val.toStringRootDot();
721,544✔
911
  }
721,544✔
912
  else
379,260✔
913
  {
379,260✔
914
    d_string+=val.toString();
379,260✔
915
  }
379,260✔
916
}
1,100,804✔
917

918
void RecordTextWriter::xfrBlobNoSpaces(const string& val, int size)
919
{
131✔
920
  xfrBlob(val, size);
131✔
921
}
131✔
922

923
void RecordTextWriter::xfrBlob(const string& val, int)
924
{
901,064✔
925
  if(!d_string.empty())
901,064✔
926
    d_string.append(1,' ');
878,199✔
927

928
  d_string+=Base64Encode(val);
901,064✔
929
}
901,064✔
930

931
void RecordTextWriter::xfrHexBlob(const string& val, bool)
932
{
267,610✔
933
  if(!d_string.empty())
267,610!
934
    d_string.append(1,' ');
267,610✔
935

936
  if(val.empty()) {
267,610✔
937
    d_string.append(1,'-');
7✔
938
    return;
7✔
939
  }
7✔
940

941
  string::size_type limit=val.size();
267,603✔
942
  char tmp[5];
267,603✔
943
  for(string::size_type n = 0; n < limit; ++n) {
932,352✔
944
    snprintf(tmp, sizeof(tmp), "%02x", (unsigned char)val[n]);
664,749✔
945
    d_string+=tmp;
664,749✔
946
  }
664,749✔
947
}
267,603✔
948

949
void RecordTextWriter::xfrSVCBValueList(const vector<string> &val) {
1,282✔
950
  bool shouldQuote{false};
1,282✔
951
  vector<string> escaped;
1,282✔
952
  escaped.reserve(val.size());
1,282✔
953
  for (auto const &v : val) {
1,562✔
954
    if (v.find_first_of(' ') != string::npos) {
1,562✔
955
      shouldQuote = true;
6✔
956
    }
6✔
957
    string tmp = txtEscape(v);
1,562✔
958
    string unescaped;
1,562✔
959
    unescaped.reserve(tmp.size() + 4);
1,562✔
960
    for (auto const &ch : tmp) {
3,334✔
961
      if (ch == '\\') {
3,334✔
962
        unescaped += R"F(\\)F";
36✔
963
        continue;
36✔
964
      }
36✔
965
      if (ch == ',') {
3,298✔
966
        unescaped += R"F(\\,)F";
30✔
967
        continue;
30✔
968
      }
30✔
969
      unescaped += ch;
3,268✔
970
    }
3,268✔
971
    escaped.push_back(std::move(unescaped));
1,562✔
972
  }
1,562✔
973
  if (shouldQuote) {
1,282✔
974
    d_string.append(1, '"');
6✔
975
  }
6✔
976
  d_string.append(boost::join(escaped, ","));
1,282✔
977
  if (shouldQuote) {
1,282✔
978
    d_string.append(1, '"');
6✔
979
  }
6✔
980
}
1,282✔
981

982
void RecordTextWriter::xfrSvcParamKeyVals(const set<SvcParam>& val) {
2,076✔
983
  for (auto const &param : val) {
4,703✔
984
    if (!d_string.empty())
4,703✔
985
      d_string.append(1, ' ');
4,649✔
986

987
    d_string.append(SvcParam::keyToString(param.getKey()));
4,703✔
988
    if (param.getKey() != SvcParam::no_default_alpn && param.getKey() != SvcParam::ohttp) {
4,703✔
989
      d_string.append(1, '=');
4,039✔
990
    }
4,039✔
991

992
    switch (param.getKey())
4,703✔
993
    {
4,703✔
994
    case SvcParam::no_default_alpn:
332✔
995
      break;
332✔
996
    case SvcParam::ipv4hint: /* fall-through */
53✔
997
    case SvcParam::ipv6hint:
80✔
998
      // TODO use xfrCA and put commas in between?
999
      if (param.getAutoHint()) {
80!
1000
        d_string.append("auto");
×
1001
        break;
×
1002
      }
×
1003
      d_string.append(ComboAddress::caContainerToString(param.getIPHints(), false));
80✔
1004
      break;
80✔
1005
    case SvcParam::alpn:
1,282✔
1006
      xfrSVCBValueList(param.getALPN());
1,282✔
1007
      break;
1,282✔
1008
    case SvcParam::mandatory:
53✔
1009
    {
53✔
1010
      bool doComma = false;
53✔
1011
      for (auto const &k: param.getMandatory()) {
62✔
1012
        if (doComma)
62✔
1013
          d_string.append(1, ',');
9✔
1014
        d_string.append(SvcParam::keyToString(k));
62✔
1015
        doComma = true;
62✔
1016
      }
62✔
1017
      break;
53✔
1018
    }
80✔
1019
    case SvcParam::port: {
1,004✔
1020
      auto str = d_string;
1,004✔
1021
      d_string.clear();
1,004✔
1022
      xfr16BitInt(param.getPort());
1,004✔
1023
      d_string = str + d_string;
1,004✔
1024
      break;
1,004✔
1025
    }
80✔
1026
    case SvcParam::ech: {
32✔
1027
      auto str = d_string;
32✔
1028
      d_string.clear();
32✔
1029
      xfrBlobNoSpaces(param.getECH());
32✔
1030
      d_string = str + '"' + d_string + '"';
32✔
1031
      break;
32✔
1032
    }
80✔
1033
    case SvcParam::ohttp:
332✔
1034
      // no value
1035
      break;
332✔
1036
    case SvcParam::tls_supported_groups: {
338✔
1037
      auto str = d_string;
338✔
1038
      d_string.clear();
338✔
1039
      bool first = true;
338✔
1040
      for (auto const &group: param.getTLSSupportedGroups()) {
676✔
1041
        if (!first) {
676✔
1042
          str += ',';
338✔
1043
        }
338✔
1044
        str += std::to_string(group);
676✔
1045
        first = false;
676✔
1046
      }
676✔
1047
      d_string = std::move(str);
338✔
1048
      break;
338✔
1049
    }
80✔
1050
    case SvcParam::dohpath:
338✔
1051
    default:
1,250✔
1052
      auto str = d_string;
1,250✔
1053
      d_string.clear();
1,250✔
1054
      xfrText(param.getValue(), false, false);
1,250✔
1055
      d_string = str + '"' + txtEscape(d_string) + '"';
1,250✔
1056
      break;
1,250✔
1057
    }
4,703✔
1058
  }
4,703✔
1059
}
2,076✔
1060

1061
void RecordTextWriter::xfrText(const string& val, bool /* multi */, bool /* lenField */)
1062
{
145,873✔
1063
  if(!d_string.empty())
145,873✔
1064
    d_string.append(1,' ');
12,122✔
1065

1066
  if (val.empty()) {
145,873!
1067
    d_string.append(2, '"');
×
1068
  }
×
1069
  else {
145,873✔
1070
    d_string.append(val);
145,873✔
1071
  }
145,873✔
1072
}
145,873✔
1073

1074
void RecordTextWriter::xfrUnquotedText(const string& val, bool /* lenField */)
1075
{
42✔
1076
  if(!d_string.empty())
42!
1077
    d_string.append(1,' ');
42✔
1078
  d_string.append(val);
42✔
1079
}
42✔
1080

1081
#ifdef TESTING
1082

1083
int main(int argc, char**argv)
1084
try
1085
{
1086
  RecordTextReader rtr(argv[1], argv[2]);
1087

1088
  unsigned int order, pref;
1089
  string flags, services, regexp, replacement;
1090
  string mx;
1091

1092
  rtr.xfrInt(order);
1093
  rtr.xfrInt(pref);
1094
  rtr.xfrText(flags);
1095
  rtr.xfrText(services);
1096
  rtr.xfrText(regexp);
1097
  rtr.xfrName(replacement);
1098

1099
  cout<<"order: "<<order<<", pref: "<<pref<<"\n";
1100
  cout<<"flags: \""<<flags<<"\", services: \""<<services<<"\", regexp: \""<<regexp<<"\", replacement: "<<replacement<<"\n";
1101

1102
  string out;
1103
  RecordTextWriter rtw(out);
1104

1105
  rtw.xfrInt(order);
1106
  rtw.xfrInt(pref);
1107
  rtw.xfrText(flags);
1108
  rtw.xfrText(services);
1109
  rtw.xfrText(regexp);
1110
  rtw.xfrName(replacement);
1111

1112
  cout<<"Regenerated: '"<<out<<"'\n";
1113

1114
}
1115
catch(std::exception& e)
1116
{
1117
  cerr<<"Fatal: "<<e.what()<<endl;
1118
}
1119

1120
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc