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

randombit / botan / 29890858783

21 Jul 2026 07:58PM UTC coverage: 89.412% (+0.008%) from 89.404%
29890858783

push

github

randombit
Update side channel document [ci skip]

Some recent improvements were not documented here.

114477 of 128033 relevant lines covered (89.41%)

10969541.35 hits per line

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

96.73
/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::from_string");
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::from_string(c.input);
9✔
49
            if(!result.test_is_true("Authority::from_string 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 + "'",
66✔
87
                                 Botan::URI::Authority::from_string(s).has_value());
44✔
88
         }
89

90
         return result;
1✔
91
      }
3✔
92

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

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

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

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

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

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

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

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

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

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

236
         return result;
1✔
237
      }
43✔
238

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

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

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

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

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

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

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

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

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

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

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

328
         return result;
1✔
329
      }
1✔
330

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

335
BOTAN_REGISTER_TEST("utils", "uri", URI_Tests);
336

337
}  // namespace
338

339
#endif
340

341
}  // namespace Botan_Tests
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc