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

randombit / botan / 21543687502

31 Jan 2026 11:19AM UTC coverage: 90.07% (-0.006%) from 90.076%
21543687502

Pull #5274

github

web-flow
Merge 39b138e01 into f044195a2
Pull Request #5274: Improve code readability in parsing utilities

102218 of 113487 relevant lines covered (90.07%)

11685802.38 hits per line

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

92.31
/src/lib/utils/parsing.cpp
1
/*
2
* Various string utils and parsing functions
3
* (C) 1999-2007,2013,2014,2015,2018 Jack Lloyd
4
* (C) 2015 Simon Warta (Kullo GmbH)
5
* (C) 2017 René Korthaus, Rohde & Schwarz Cybersecurity
6
*
7
* Botan is released under the Simplified BSD License (see license.txt)
8
*/
9

10
#include <botan/internal/parsing.h>
11

12
#include <botan/exceptn.h>
13
#include <botan/internal/fmt.h>
14
#include <botan/internal/loadstor.h>
15
#include <algorithm>
16
#include <cctype>
17
#include <limits>
18
#include <sstream>
19

20
namespace Botan {
21

22
uint16_t to_uint16(std::string_view str) {
391✔
23
   const uint32_t x = to_u32bit(str);
391✔
24

25
   if(x != static_cast<uint16_t>(x)) {
391✔
26
      throw Invalid_Argument("Integer value exceeds 16 bit range");
×
27
   }
28

29
   return static_cast<uint16_t>(x);
391✔
30
}
31

32
uint32_t to_u32bit(std::string_view str_view) {
386,009✔
33
   const std::string str(str_view);
386,009✔
34

35
   // std::stoul is not strict enough. Ensure that str is digit only [0-9]*
36
   for(const char chr : str) {
1,190,429✔
37
      if(chr < '0' || chr > '9') {
805,042✔
38
         throw Invalid_Argument("to_u32bit invalid decimal string '" + str + "'");
1,866✔
39
      }
40
   }
41

42
   const unsigned long int x = std::stoul(str);
385,387✔
43

44
   if constexpr(sizeof(unsigned long int) > 4) {
385,387✔
45
      // x might be uint64
46
      if(x > std::numeric_limits<uint32_t>::max()) {
385,387✔
47
         throw Invalid_Argument("Integer value of " + str + " exceeds 32 bit range");
×
48
      }
49
   }
50

51
   return static_cast<uint32_t>(x);
385,387✔
52
}
386,009✔
53

54
/*
55
* Parse a SCAN-style algorithm name
56
*/
57
std::vector<std::string> parse_algorithm_name(std::string_view scan_name) {
18,277✔
58
   if(scan_name.find('(') == std::string::npos && scan_name.find(')') == std::string::npos) {
18,277✔
59
      return {std::string(scan_name)};
34,504✔
60
   }
61

62
   std::string name(scan_name);
1,025✔
63
   std::string substring;
1,025✔
64
   std::vector<std::string> elems;
1,025✔
65
   size_t level = 0;
1,025✔
66

67
   elems.push_back(name.substr(0, name.find('(')));
2,050✔
68
   name = name.substr(name.find('('));
1,025✔
69

70
   for(auto i = name.begin(); i != name.end(); ++i) {
4,187✔
71
      const char c = *i;
4,187✔
72

73
      if(c == '(') {
4,187✔
74
         ++level;
1,025✔
75
      }
76
      if(c == ')') {
4,187✔
77
         if(level == 1 && i == name.end() - 1) {
1,025✔
78
            if(elems.size() == 1) {
1,025✔
79
               elems.push_back(substring.substr(1));
1,630✔
80
            } else {
81
               elems.push_back(substring);
210✔
82
            }
83
            return elems;
1,025✔
84
         }
85

86
         if(level == 0 || (level == 1 && i != name.end() - 1)) {
×
87
            throw Invalid_Algorithm_Name(scan_name);
×
88
         }
89
         --level;
×
90
      }
91

92
      if(c == ',' && level == 1) {
3,162✔
93
         if(elems.size() == 1) {
210✔
94
            elems.push_back(substring.substr(1));
420✔
95
         } else {
96
            elems.push_back(substring);
×
97
         }
98
         substring.clear();
210✔
99
      } else {
100
         substring += c;
6,114✔
101
      }
102
   }
103

104
   if(!substring.empty()) {
×
105
      throw Invalid_Algorithm_Name(scan_name);
×
106
   }
107

108
   return elems;
×
109
}
35,529✔
110

111
std::vector<std::string> split_on(std::string_view str, char delim) {
222,290✔
112
   std::vector<std::string> elems;
222,290✔
113
   if(str.empty()) {
222,290✔
114
      return elems;
115
   }
116

117
   std::string substr;
222,170✔
118
   for(const char c : str) {
2,875,303✔
119
      if(c == delim) {
2,653,133✔
120
         if(!substr.empty()) {
172,029✔
121
            elems.push_back(substr);
171,987✔
122
         }
123
         substr.clear();
172,029✔
124
      } else {
125
         substr += c;
5,134,237✔
126
      }
127
   }
128

129
   if(substr.empty()) {
222,170✔
130
      throw Invalid_Argument(fmt("Unable to split string '{}", str));
2✔
131
   }
132
   elems.push_back(substr);
222,169✔
133

134
   return elems;
222,169✔
135
}
222,170✔
136

137
/*
138
* Join a string
139
*/
140
std::string string_join(const std::vector<std::string>& strs, char delim) {
19✔
141
   std::ostringstream out;
19✔
142

143
   for(size_t i = 0; i != strs.size(); ++i) {
236✔
144
      if(i != 0) {
217✔
145
         out << delim;
198✔
146
      }
147
      out << strs[i];
217✔
148
   }
149

150
   return out.str();
38✔
151
}
19✔
152

153
/*
154
* Convert a decimal-dotted string to binary IP
155
*/
156
std::optional<uint32_t> string_to_ipv4(std::string_view str) {
5,373✔
157
   // At least 3 dots + 4 1-digit integers
158
   // At most 3 dots + 4 3-digit integers
159
   if(str.size() < 3 + 4 * 1 || str.size() > 3 + 4 * 3) {
5,373✔
160
      return {};
1,692✔
161
   }
162

163
   // the final result
164
   uint32_t ip = 0;
3,681✔
165
   // the number of '.' seen so far
166
   size_t dots = 0;
3,681✔
167
   // accumulates one quad (range 0-255)
168
   uint32_t accum = 0;
3,681✔
169
   // # of digits pushed to accum since last dot
170
   size_t cur_digits = 0;
3,681✔
171

172
   for(const char c : str) {
4,027✔
173
      if(c == '.') {
4,002✔
174
         // . without preceding digit is invalid
175
         if(cur_digits == 0) {
122✔
176
            return {};
6✔
177
         }
178
         dots += 1;
116✔
179
         // too many dots
180
         if(dots > 3) {
116✔
181
            return {};
6✔
182
         }
183

184
         cur_digits = 0;
110✔
185
         ip = (ip << 8) | accum;
110✔
186
         accum = 0;
110✔
187
      } else if(c >= '0' && c <= '9') {
3,880✔
188
         const auto d = static_cast<uint8_t>(c - '0');
247✔
189

190
         // prohibit leading zero in quad (used for octal)
191
         if(cur_digits > 0 && accum == 0) {
247✔
192
            return {};
7✔
193
         }
194
         accum = (accum * 10) + d;
240✔
195

196
         if(accum > 255) {
240✔
197
            return {};
4✔
198
         }
199

200
         cur_digits++;
236✔
201
         BOTAN_ASSERT_NOMSG(cur_digits <= 3);
236✔
202
      } else {
203
         return {};
3,633✔
204
      }
205
   }
206

207
   // no trailing digits?
208
   if(cur_digits == 0) {
25✔
209
      return {};
×
210
   }
211

212
   // insufficient # of dots
213
   if(dots != 3) {
25✔
214
      return {};
×
215
   }
216

217
   ip = (ip << 8) | accum;
25✔
218

219
   return ip;
25✔
220
}
221

222
/*
223
* Convert an IP address to decimal-dotted string
224
*/
225
std::string ipv4_to_string(uint32_t ip) {
20✔
226
   uint8_t bits[4];
20✔
227
   store_be(ip, bits);
20✔
228

229
   std::string str;
20✔
230

231
   for(size_t i = 0; i != 4; ++i) {
100✔
232
      if(i > 0) {
80✔
233
         str += ".";
60✔
234
      }
235
      str += std::to_string(bits[i]);
160✔
236
   }
237

238
   return str;
20✔
239
}
×
240

241
std::string tolower_string(std::string_view str) {
2,406✔
242
   std::string lower(str);
2,406✔
243
   for(char& c : lower) {
43,276✔
244
      const int cu = static_cast<unsigned char>(c);
40,870✔
245
      if(std::isalpha(cu) != 0) {
40,870✔
246
         c = static_cast<char>(std::tolower(cu));
35,955✔
247
      }
248
   }
249
   return lower;
2,406✔
250
}
251

252
bool host_wildcard_match(std::string_view issued_, std::string_view host_) {
255✔
253
   const std::string issued = tolower_string(issued_);
255✔
254
   const std::string host = tolower_string(host_);
255✔
255

256
   if(host.empty() || issued.empty()) {
255✔
257
      return false;
258
   }
259

260
   /*
261
   If there are embedded nulls in your issued name
262
   Well I feel bad for you son
263
   */
264
   if(std::count(issued.begin(), issued.end(), char(0)) > 0) {
504✔
265
      return false;
266
   }
267

268
   // If more than one wildcard, then issued name is invalid
269
   const size_t stars = std::count(issued.begin(), issued.end(), '*');
252✔
270
   if(stars > 1) {
252✔
271
      return false;
272
   }
273

274
   // '*' is not a valid character in DNS names so should not appear on the host side
275
   if(std::count(host.begin(), host.end(), '*') != 0) {
492✔
276
      return false;
277
   }
278

279
   // Similarly a DNS name can't end in .
280
   if(host[host.size() - 1] == '.') {
243✔
281
      return false;
282
   }
283

284
   // And a host can't have an empty name component, so reject that
285
   if(host.find("..") != std::string::npos) {
243✔
286
      return false;
287
   }
288

289
   // Exact match: accept
290
   if(issued == host) {
241✔
291
      return true;
292
   }
293

294
   /*
295
   Otherwise it might be a wildcard
296

297
   If the issued size is strictly longer than the hostname size it
298
   couldn't possibly be a match, even if the issued value is a
299
   wildcard. The only exception is when the wildcard ends up empty
300
   (eg www.example.com matches www*.example.com)
301
   */
302
   if(issued.size() > host.size() + 1) {
60✔
303
      return false;
304
   }
305

306
   // If no * at all then not a wildcard, and so not a match
307
   if(stars != 1) {
48✔
308
      return false;
309
   }
310

311
   /*
312
   Now walk through the issued string, making sure every character
313
   matches. When we come to the (singular) '*', jump forward in the
314
   hostname by the corresponding amount. We know exactly how much
315
   space the wildcard takes because it must be exactly `len(host) -
316
   len(issued) + 1 chars`.
317

318
   We also verify that the '*' comes in the leftmost component, and
319
   doesn't skip over any '.' in the hostname.
320
   */
321
   size_t dots_seen = 0;
322
   size_t host_idx = 0;
323

324
   for(size_t i = 0; i != issued.size(); ++i) {
326✔
325
      if(issued[i] == '.') {
309✔
326
         dots_seen += 1;
44✔
327
      }
328

329
      if(issued[i] == '*') {
309✔
330
         // Fail: wildcard can only come in leftmost component
331
         if(dots_seen > 0) {
39✔
332
            return false;
333
         }
334

335
         /*
336
         Since there is only one * we know the tail of the issued and
337
         hostname must be an exact match. In this case advance host_idx
338
         to match.
339
         */
340
         const size_t advance = (host.size() - issued.size() + 1);
28✔
341

342
         if(host_idx + advance > host.size()) {  // shouldn't happen
28✔
343
            return false;
344
         }
345

346
         // Can't be any intervening .s that we would have skipped
347
         if(std::count(host.begin() + host_idx, host.begin() + host_idx + advance, '.') != 0) {
56✔
348
            return false;
349
         }
350

351
         host_idx += advance;
352
      } else {
353
         if(issued[i] != host[host_idx]) {
270✔
354
            return false;
355
         }
356

357
         host_idx += 1;
267✔
358
      }
359
   }
360

361
   // Wildcard issued name must have at least 3 components
362
   return (dots_seen >= 2);
17✔
363
}
255✔
364

365
std::string check_and_canonicalize_dns_name(std::string_view name) {
10,698✔
366
   if(name.size() > 255) {
10,698✔
367
      throw Decoding_Error("DNS name exceeds maximum allowed length");
85✔
368
   }
369

370
   if(name.empty()) {
10,613✔
371
      throw Decoding_Error("DNS name cannot be empty");
5✔
372
   }
373

374
   if(name.starts_with(".") || name.ends_with(".")) {
10,608✔
375
      throw Decoding_Error("DNS name cannot start or end with a dot");
7✔
376
   }
377

378
   /*
379
   * Table mapping uppercase to lowercase and only including values for valid DNS names
380
   * namely A-Z, a-z, 0-9, hyphen, and dot, plus '*' for wildcarding. (RFC 1035)
381
   */
382
   // clang-format off
383
   constexpr uint8_t DNS_CHAR_MAPPING[128] = {
10,601✔
384
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
385
      '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0',
386
      '\0', '\0', '\0', '\0',  '*', '\0', '\0',  '-',  '.', '\0',  '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',  '8',
387
       '9', '\0', '\0', '\0', '\0', '\0', '\0', '\0',  'a',  'b',  'c',  'd',  'e',  'f',  'g',  'h',  'i',  'j',  'k',
388
       'l',  'm',  'n',  'o',  'p',  'q',  'r',  's',  't',  'u',  'v',  'w',  'x',  'y',  'z', '\0', '\0', '\0', '\0',
389
      '\0', '\0',  'a',  'b',  'c',  'd',  'e',  'f',  'g',  'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',  'p',  'q',
390
       'r',  's',  't',  'u',  'v',  'w',  'x',  'y',  'z', '\0', '\0', '\0', '\0', '\0',
391
   };
392
   // clang-format on
393

394
   std::string canon;
10,601✔
395
   canon.reserve(name.size());
10,601✔
396

397
   // RFC 1035: DNS labels must not exceed 63 characters
398
   size_t current_label_length = 0;
399

400
   for(size_t i = 0; i != name.size(); ++i) {
128,781✔
401
      const char c = name[i];
118,587✔
402

403
      if(c == '.') {
118,587✔
404
         if(i > 0 && name[i - 1] == '.') {
9,387✔
405
            throw Decoding_Error("DNS name contains sequential period chars");
4✔
406
         }
407

408
         if(current_label_length == 0) {
9,383✔
409
            throw Decoding_Error("DNS name contains empty label");
×
410
         }
411
         current_label_length = 0;  // Reset for next label
412
      } else {
413
         current_label_length++;
109,200✔
414

415
         if(current_label_length > 63) {  // RFC 1035 Maximum DNS label length
109,200✔
416
            throw Decoding_Error("DNS name label exceeds maximum length of 63 characters");
1✔
417
         }
418
      }
419

420
      const uint8_t cu = static_cast<uint8_t>(c);
118,582✔
421
      if(cu >= 128) {
118,582✔
422
         throw Decoding_Error("DNS name must not contain any extended ASCII code points");
132✔
423
      }
424
      const uint8_t mapped = DNS_CHAR_MAPPING[cu];
118,450✔
425
      if(mapped == 0) {
118,450✔
426
         throw Decoding_Error("DNS name includes invalid character");
263✔
427
      }
428

429
      if(mapped == '-') {
118,187✔
430
         if(i == 0 || (i > 0 && name[i - 1] == '.')) {
672✔
431
            throw Decoding_Error("DNS name has label with leading hyphen");
4✔
432
         } else if(i == name.size() - 1 || (i < name.size() - 1 && name[i + 1] == '.')) {
668✔
433
            throw Decoding_Error("DNS name has label with trailing hyphen");
3✔
434
         }
435
      }
436
      canon.push_back(static_cast<char>(mapped));
118,180✔
437
   }
438

439
   if(current_label_length == 0) {
10,194✔
440
      throw Decoding_Error("DNS name contains empty label");
×
441
   }
442
   return canon;
10,194✔
443
}
407✔
444

445
}  // namespace Botan
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

© 2026 Coveralls, Inc