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

randombit / botan / 27930394332

22 Jun 2026 02:29AM UTC coverage: 89.361% (-2.3%) from 91.664%
27930394332

push

github

randombit
Escape control chars in X509_Certificate::to_string

111792 of 125101 relevant lines covered (89.36%)

10818223.53 hits per line

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

96.71
/src/tests/test_uri.cpp
1
/*
2
* (C) 2019 Nuno Goncalves <nunojpg@gmail.com>
3
*     2023,2024,2026 Jack Lloyd
4
*
5
* Botan is released under the Simplified BSD License (see license.txt)
6
*/
7

8
#include "tests.h"
9

10
#if defined(BOTAN_HAS_URI)
11
   #include <botan/uri.h>
12
#endif
13

14
namespace Botan_Tests {
15

16
#if defined(BOTAN_HAS_URI)
17

18
namespace {
19

20
class URI_Tests final : public Test {
1✔
21
   private:
22
      using HostKind = Botan::URI::Authority::HostKind;
23

24
      static Test::Result test_authority_parse() {
1✔
25
         Test::Result result("URI::Authority::parse");
1✔
26

27
         struct Case {
18✔
28
               std::string input;
29
               std::string host;
30
               std::optional<uint16_t> port;
31
               HostKind kind;
32
         };
33

34
         const std::vector<Case> cases{
1✔
35
            {"localhost:80", "localhost", 80, HostKind::DNS},
36
            {"www.example.com", "www.example.com", std::nullopt, HostKind::DNS},
37
            {"192.168.1.1", "192.168.1.1", std::nullopt, HostKind::IPv4},
38
            {"192.168.1.1:34567", "192.168.1.1", 34567, HostKind::IPv4},
39
            {"[::1]:61234", "::1", 61234, HostKind::IPv6},
40
            {"[::1]", "::1", std::nullopt, HostKind::IPv6},
41
            {"Example.COM:443", "example.com", 443, HostKind::DNS},
42
            // Userinfo is preserved in original_input()
43
            {"user:pw@example.com:8443", "example.com", 8443, HostKind::DNS},
44
            {"alice@example.com", "example.com", std::nullopt, HostKind::DNS},
45
         };
11✔
46

47
         for(const auto& c : cases) {
10✔
48
            const auto authority = Botan::URI::Authority::parse(c.input);
9✔
49
            if(!result.test_is_true("Authority::parse succeeds: " + c.input, authority.has_value())) {
9✔
50
               continue;
×
51
            }
52
            result.test_str_eq("host: " + c.input, authority->host_to_string(), c.host);
18✔
53
            result.test_opt_u16_eq("port: " + c.input, authority->port(), c.port);
9✔
54
            result.test_is_true("host kind: " + c.input, authority->host_kind() == c.kind);
9✔
55
            result.test_str_eq("original input: " + c.input, authority->original_input(), c.input);
18✔
56
         }
9✔
57

58
         const std::vector<std::string> invalid = {
1✔
59
            "",
60
            "localhost::80",
61
            "localhost:80aa",
62
            "localhost:%50",
63
            "localhost:70000",
64
            "localhost:0",
65
            // Ports may not have leading zeros
66
            "localhost:0080",
67
            "localhost:007",
68
            "192.168.1.1:08080",
69
            "[::1]:0443",
70
            "[::1]:a",
71
            "[::1]:70000",
72
            "hello..com",
73
            ".leading.dot",
74
            "[not-an-ipv6]:80",
75
            "[::1",
76
            "::1]:80",
77
            "host space:80",
78
            // Trailing dot is theoretically valid, but rejected
79
            "host.example.com.",
80
            "192.168.1.1.",
81
            "192.168.1.1.:8080",
82
            // _ not valid in host names, only DNS SRV records which is not relevant here
83
            "_acme-challenge.example.com",
84
         };
1✔
85
         for(const auto& s : invalid) {
23✔
86
            result.test_is_false("rejects invalid authority '" + s + "'", Botan::URI::Authority::parse(s).has_value());
66✔
87
         }
88

89
         return result;
1✔
90
      }
3✔
91

92
      static Test::Result test_parse() {
1✔
93
         Test::Result result("URI::parse");
1✔
94

95
         struct Case {
1✔
96
               std::string input;
97
               std::string scheme;
98
               std::string host;
99
               std::optional<uint16_t> port;
100
               HostKind kind;
101
         };
102

103
         const std::vector<Case> cases{
1✔
104
            {"https://foo.example.com/", "https", "foo.example.com", std::nullopt, HostKind::DNS},
1✔
105
            {"http://foo.example.com:8080/path?q=1#frag", "http", "foo.example.com", 8080, HostKind::DNS},
106
            {"https://[2001:db8::1]/", "https", "2001:db8::1", std::nullopt, HostKind::IPv6},
107
            {"https://10.0.0.1/", "https", "10.0.0.1", std::nullopt, HostKind::IPv4},
108
            {"https://user:pw@sub.example.com:8443/path", "https", "sub.example.com", 8443, HostKind::DNS},
109
            {"HTTPS://Example.COM/", "https", "example.com", std::nullopt, HostKind::DNS},
110
         };
7✔
111

112
         for(const auto& c : cases) {
7✔
113
            const auto uri = Botan::URI::parse(c.input);
6✔
114
            if(!result.test_is_true("parse succeeds: " + c.input, uri.has_value())) {
6✔
115
               continue;
×
116
            }
117
            result.test_str_eq("scheme: " + c.input, uri->scheme(), c.scheme);
6✔
118
            if(result.test_is_true("authority present: " + c.input, uri->authority().has_value())) {
6✔
119
               const auto authority = uri->authority().value();
6✔
120
               const auto raw_authority = uri->raw_authority();
6✔
121
               result.test_is_true("raw authority present: " + c.input, raw_authority.has_value());
6✔
122
               if(raw_authority.has_value()) {
6✔
123
                  result.test_str_eq(
6✔
124
                     "raw authority: " + c.input, std::string(*raw_authority), authority.original_input());
18✔
125
               }
126
               result.test_str_eq("host: " + c.input, authority.host_to_string(), c.host);
12✔
127
               result.test_opt_u16_eq("port: " + c.input, authority.port(), c.port);
6✔
128
               result.test_enum_eq("host kind: " + c.input, authority.host_kind(), c.kind);
6✔
129
            }
6✔
130
         }
6✔
131

132
         struct NoAuthorityCase {
1✔
133
               std::string input;
134
               std::string scheme;
135
               std::string path;
136
               std::optional<std::string> query;
137
               std::optional<std::string> fragment;
138
         };
139

140
         const std::vector<NoAuthorityCase> no_authority_cases{
1✔
141
            {"urn:ashes", "urn", "ashes", std::nullopt, std::nullopt},
1✔
142
            {"mailto:root@attacker.com", "mailto", "root@attacker.com", std::nullopt, std::nullopt},
143
            {"tel:867-5309", "tel", "867-5309", std::nullopt, std::nullopt},
144
            {"foo:", "foo", "", std::nullopt, std::nullopt},
145
            {"foo:/path?q=1#frag", "foo", "/path", "q=1", "frag"},
146
         };
6✔
147

148
         for(const auto& c : no_authority_cases) {
6✔
149
            const auto uri = Botan::URI::parse(c.input);
5✔
150
            if(!result.test_is_true("parse succeeds without authority: " + c.input, uri.has_value())) {
5✔
151
               continue;
×
152
            }
153
            result.test_is_false("authority absent: " + c.input, uri->authority().has_value());
5✔
154
            result.test_is_false("raw authority absent: " + c.input, uri->raw_authority().has_value());
5✔
155
            result.test_str_eq("scheme: " + c.input, uri->scheme(), c.scheme);
5✔
156
            result.test_is_false("host absent: " + c.input, uri->host().has_value());
10✔
157
            result.test_str_eq("path: " + c.input, uri->path(), c.path);
5✔
158
            result.test_bool_eq("query presence: " + c.input, uri->query().has_value(), c.query.has_value());
5✔
159
            if(c.query.has_value() && uri->query().has_value()) {
5✔
160
               result.test_str_eq("query: " + c.input, *uri->query(), *c.query);
2✔
161
            }
162
            result.test_bool_eq("fragment presence: " + c.input, uri->fragment().has_value(), c.fragment.has_value());
5✔
163
            if(c.fragment.has_value() && uri->fragment().has_value()) {
5✔
164
               result.test_str_eq("fragment: " + c.input, *uri->fragment(), *c.fragment);
2✔
165
            }
166
         }
5✔
167

168
         const std::vector<NoAuthorityCase> empty_authority_cases{
1✔
169
            {"ldap:///CN=Example,C=US?cACertificate?base?objectClass=certificationAuthority",
1✔
170
             "ldap",
171
             "/CN=Example,C=US",
172
             "cACertificate?base?objectClass=certificationAuthority",
173
             std::nullopt},
174
            {"ldaps:///CN=Example", "ldaps", "/CN=Example", std::nullopt, std::nullopt},
175
            {"file:///tmp/cert.pem", "file", "/tmp/cert.pem", std::nullopt, std::nullopt},
176
            {"http:///path", "http", "/path", std::nullopt, std::nullopt},
177
            {"https://", "https", "", std::nullopt, std::nullopt},
178
            {"https:///path", "https", "/path", std::nullopt, std::nullopt},
179
         };
7✔
180

181
         for(const auto& c : empty_authority_cases) {
7✔
182
            const auto uri = Botan::URI::parse(c.input);
6✔
183
            if(!result.test_is_true("parse succeeds with empty authority: " + c.input, uri.has_value())) {
6✔
184
               continue;
×
185
            }
186
            result.test_is_false("parsed authority absent: " + c.input, uri->authority().has_value());
6✔
187
            const auto raw_authority = uri->raw_authority();
6✔
188
            result.test_is_true("raw authority present: " + c.input, raw_authority.has_value());
6✔
189
            if(raw_authority.has_value()) {
6✔
190
               result.test_str_eq("raw authority is empty: " + c.input, std::string(*raw_authority), "");
18✔
191
            }
192
            result.test_str_eq("scheme: " + c.input, uri->scheme(), c.scheme);
6✔
193
            result.test_is_false("host absent: " + c.input, uri->host().has_value());
12✔
194
            result.test_str_eq("path: " + c.input, uri->path(), c.path);
6✔
195
            result.test_bool_eq("query presence: " + c.input, uri->query().has_value(), c.query.has_value());
6✔
196
            if(c.query.has_value() && uri->query().has_value()) {
6✔
197
               result.test_str_eq("query: " + c.input, *uri->query(), *c.query);
2✔
198
            }
199
            result.test_bool_eq("fragment presence: " + c.input, uri->fragment().has_value(), c.fragment.has_value());
6✔
200
            if(c.fragment.has_value() && uri->fragment().has_value()) {
6✔
201
               result.test_str_eq("fragment: " + c.input, *uri->fragment(), *c.fragment);
×
202
            }
203
         }
6✔
204

205
         const std::vector<std::string> invalid = {
1✔
206
            "",
207
            "://no.scheme/",
208
            "1http://host/",
209
            "https//no-colon/",
210
            "https://[not-an-ip]/",
211
            "https://example.com:0443/",
212
            // Path/query/fragment must use RFC 3986 character set.
213
            "https://example.com/has space",
214
            "https://example.com/path<bracket>",
215
            "https://example.com/%G0",
216
            "https://example.com/%2",
217
            // Percent encoded embedded nulls are rejected
218
            "https://example.com/embedded/null/%00/surprise",
219
            // Fragment delimiter may appear at most once
220
            "https://example.com/path#frag#extra",
221
            "https://example.com/#a#b",
222
            // RFC 3986 userinfo does not allow unencoded '@'.
223
            "https://user@bad@example.com/",
224
            // Userinfo character set validation:
225
            "https://user name@example.com/",
226
            "https://user<x>@example.com/",
227
            "https://user\xff@example.com/",
228
            "https://user%G0@example.com/",
229
            "https://user%00null@example.com/",
230
         };
1✔
231
         for(const auto& s : invalid) {
20✔
232
            result.test_is_false("rejects invalid URI '" + s + "'", Botan::URI::parse(s).has_value());
57✔
233
         }
234

235
         return result;
1✔
236
      }
43✔
237

238
      static Test::Result test_equality() {
1✔
239
         Test::Result result("URI equality semantics");
1✔
240

241
         // Two URIs that share scheme + host + port but differ in path
242
         // are distinct identities. Critical for SPIFFE-style workload
243
         // IDs encoded as URI SANs - otherwise `uri_names().contains()`
244
         // could be satisfied by the wrong workload identity.
245
         const auto a = Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value();
2✔
246
         const auto b = Botan::URI::parse("spiffe://trust.example/ns/prod/sa/server").value();
2✔
247
         result.test_is_false("SPIFFE: differing paths are not equal", a == b);
1✔
248
         result.test_is_true("SPIFFE: equal with the same path",
2✔
249
                             a == Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value());
2✔
250

251
         // Scheme and host casing don't break equality (we canonicalize).
252
         result.test_is_true("scheme/host case folded for equality",
2✔
253
                             Botan::URI::parse("HTTPS://Example.COM/path").value() ==
3✔
254
                                Botan::URI::parse("https://example.com/path").value());
2✔
255

256
         // Path case IS significant (RFC 3986 - paths are not
257
         // case-canonicalized).
258
         result.test_is_false("path case is significant",
2✔
259
                              Botan::URI::parse("https://example.com/Path").value() ==
3✔
260
                                 Botan::URI::parse("https://example.com/path").value());
2✔
261

262
         // Userinfo is preserved verbatim and participates in identity
263
         // (RFC 3986 6.2 case-normalizes only scheme and host; RFC 5280
264
         // 7.4 requires URI comparison to be exact-match after that).
265
         const auto uri_with_userinfo = Botan::URI::parse("https://alice:s3cret@example.com/").value();
2✔
266

267
         result.test_is_true("userinfo distinguishes identity",
2✔
268
                             uri_with_userinfo != Botan::URI::parse("https://example.com/").value());
2✔
269
         result.test_is_true("userinfo equal when matching",
2✔
270
                             uri_with_userinfo == Botan::URI::parse("https://alice:s3cret@example.com/").value());
2✔
271
         // The authority's original_input() includes the userinfo
272
         result.test_is_true("authority present with userinfo", uri_with_userinfo.authority().has_value());
1✔
273
         result.test_str_eq("authority original_input preserves userinfo",
1✔
274
                            uri_with_userinfo.authority()->original_input(),
1✔
275
                            "alice:s3cret@example.com");
276
         // Userinfo case IS significant (no case normalization).
277
         result.test_is_false("userinfo case is significant",
2✔
278
                              Botan::URI::parse("https://Alice@example.com/").value() ==
3✔
279
                                 Botan::URI::parse("https://alice@example.com/").value());
2✔
280
         // Empty userinfo is distinct from no userinfo (RFC 3986
281
         // authority grammar: "@" delimiter presence is significant).
282
         const auto absent = Botan::URI::parse("https://example.com/").value();
2✔
283
         const auto empty = Botan::URI::parse("https://@example.com/").value();
2✔
284
         result.test_is_true("empty userinfo != absent userinfo", absent != empty);
1✔
285
         result.test_is_false("absent userinfo: accessor reports nullopt", absent.authority()->userinfo().has_value());
1✔
286
         result.test_is_true("empty userinfo: accessor reports present", empty.authority()->userinfo().has_value());
1✔
287
         result.test_str_eq("empty userinfo: accessor reports empty string", *empty.authority()->userinfo(), "");
1✔
288

289
         // Path, query, and fragment are split out and exposed separately.
290
         const auto full = Botan::URI::parse("https://example.com/path?q=1#frag").value();
2✔
291
         result.test_str_eq("path component", full.path(), "/path");
1✔
292
         result.test_is_true("query present", full.query().has_value());
1✔
293
         result.test_str_eq("query component", *full.query(), "q=1");
1✔
294
         result.test_is_true("fragment present", full.fragment().has_value());
1✔
295
         result.test_str_eq("fragment component", *full.fragment(), "frag");
1✔
296

297
         // Empty path is preserved as empty (not defaulted to "/").
298
         const auto no_path = Botan::URI::parse("https://example.com").value();
2✔
299
         result.test_str_eq("empty path stays empty", no_path.path(), "");
1✔
300
         result.test_is_false("no query", no_path.query().has_value());
1✔
301
         result.test_is_false("no fragment", no_path.fragment().has_value());
1✔
302

303
         const auto mailto = Botan::URI::parse("mailto:root@example.com").value();
2✔
304
         result.test_is_false("mailto has no authority", mailto.authority().has_value());
1✔
305
         result.test_is_false("mailto has no raw authority", mailto.raw_authority().has_value());
1✔
306
         result.test_is_false(
2✔
307
            "authorityful URI differs from authorityless URI",
308
            Botan::URI::parse("foo://example.com/path").value() == Botan::URI::parse("foo:/path").value());
4✔
309
         result.test_is_false("empty authority differs from absent authority",
2✔
310
                              Botan::URI::parse("foo:///path").value() == Botan::URI::parse("foo:/path").value());
4✔
311

312
         // Query without path: empty path, present query.
313
         const auto query_only = Botan::URI::parse("https://example.com?q=1").value();
2✔
314
         result.test_str_eq("query-only: path is empty", query_only.path(), "");
1✔
315
         result.test_is_true("query-only: query present", query_only.query().has_value());
1✔
316
         result.test_str_eq("query-only: query value", *query_only.query(), "q=1");
1✔
317

318
         // Present-but-empty query / fragment are distinct from absent.
319
         const auto empty_query = Botan::URI::parse("https://example.com/p?").value();
2✔
320
         result.test_is_true("empty query: present", empty_query.query().has_value());
1✔
321
         result.test_str_eq("empty query: value", *empty_query.query(), "");
1✔
322
         const auto empty_frag = Botan::URI::parse("https://example.com/p#").value();
2✔
323
         result.test_is_true("empty fragment: present", empty_frag.fragment().has_value());
1✔
324
         result.test_str_eq("empty fragment: value", *empty_frag.fragment(), "");
1✔
325

326
         return result;
1✔
327
      }
1✔
328

329
   public:
330
      std::vector<Test::Result> run() override { return {test_authority_parse(), test_parse(), test_equality()}; }
4✔
331
};
332

333
BOTAN_REGISTER_TEST("utils", "uri", URI_Tests);
334

335
}  // namespace
336

337
#endif
338

339
}  // namespace Botan_Tests
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