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

randombit / botan / 27456950099

12 Jun 2026 07:59PM UTC coverage: 89.424% (+0.05%) from 89.378%
27456950099

push

github

web-flow
Merge pull request #5663 from randombit/jack/dns-uri-ip-fixes

Bugfixes and enhancements for DNSName, URI, IPv4Address, IPv6Address

111165 of 124312 relevant lines covered (89.42%)

10989620.56 hits per line

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

97.09
/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 void test_port(Test::Result& result,
15✔
25
                            std::string_view what,
26
                            const std::optional<uint16_t>& got,
27
                            const std::optional<uint16_t>& expected) {
28
         if(got.has_value() != expected.has_value()) {
15✔
29
            result.test_failure(std::string(what) + ": port presence mismatch");
×
30
         } else if(got.has_value()) {
15✔
31
            result.test_u16_eq(what, *got, *expected);
7✔
32
         } else {
33
            result.test_success(std::string(what) + ": both nullopt");
24✔
34
         }
35
      }
23✔
36

37
      static Test::Result test_authority_parse() {
1✔
38
         Test::Result result("URI::Authority::parse");
1✔
39

40
         struct Case {
18✔
41
               std::string input;
42
               std::string host;
43
               std::optional<uint16_t> port;
44
               HostKind kind;
45
         };
46

47
         const std::vector<Case> cases{
1✔
48
            {"localhost:80", "localhost", 80, HostKind::DNS},
1✔
49
            {"www.example.com", "www.example.com", std::nullopt, HostKind::DNS},
50
            {"192.168.1.1", "192.168.1.1", std::nullopt, HostKind::IPv4},
51
            {"192.168.1.1:34567", "192.168.1.1", 34567, HostKind::IPv4},
52
            {"[::1]:61234", "::1", 61234, HostKind::IPv6},
53
            {"[::1]", "::1", std::nullopt, HostKind::IPv6},
54
            {"Example.COM:443", "example.com", 443, HostKind::DNS},
55
            // Userinfo is preserved in original_input()
56
            {"user:pw@example.com:8443", "example.com", 8443, HostKind::DNS},
57
            {"alice@example.com", "example.com", std::nullopt, HostKind::DNS},
58
         };
10✔
59

60
         for(const auto& c : cases) {
10✔
61
            const auto authority = Botan::URI::Authority::parse(c.input);
9✔
62
            if(!result.test_is_true("Authority::parse succeeds: " + c.input, authority.has_value())) {
9✔
63
               continue;
×
64
            }
65
            result.test_str_eq("host: " + c.input, authority->host_to_string(), c.host);
18✔
66
            test_port(result, "port: " + c.input, authority->port(), c.port);
9✔
67
            result.test_is_true("host kind: " + c.input, authority->host_kind() == c.kind);
9✔
68
            result.test_str_eq("original input: " + c.input, authority->original_input(), c.input);
18✔
69
         }
9✔
70

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

102
         return result;
1✔
103
      }
3✔
104

105
      static Test::Result test_parse() {
1✔
106
         Test::Result result("URI::parse");
1✔
107

108
         struct Case {
1✔
109
               std::string input;
110
               std::string scheme;
111
               std::string host;
112
               std::optional<uint16_t> port;
113
               HostKind kind;
114
         };
115

116
         const std::vector<Case> cases{
1✔
117
            {"https://foo.example.com/", "https", "foo.example.com", std::nullopt, HostKind::DNS},
1✔
118
            {"http://foo.example.com:8080/path?q=1#frag", "http", "foo.example.com", 8080, HostKind::DNS},
119
            {"https://[2001:db8::1]/", "https", "2001:db8::1", std::nullopt, HostKind::IPv6},
120
            {"https://10.0.0.1/", "https", "10.0.0.1", std::nullopt, HostKind::IPv4},
121
            {"https://user:pw@sub.example.com:8443/path", "https", "sub.example.com", 8443, HostKind::DNS},
122
            {"HTTPS://Example.COM/", "https", "example.com", std::nullopt, HostKind::DNS},
123
         };
7✔
124

125
         for(const auto& c : cases) {
7✔
126
            const auto uri = Botan::URI::parse(c.input);
6✔
127
            if(!result.test_is_true("parse succeeds: " + c.input, uri.has_value())) {
6✔
128
               continue;
×
129
            }
130
            result.test_str_eq("scheme: " + c.input, uri->scheme(), c.scheme);
6✔
131
            result.test_str_eq("host: " + c.input, uri->host_to_string(), c.host);
12✔
132
            test_port(result, "port: " + c.input, uri->port(), c.port);
6✔
133
            result.test_is_true("host kind: " + c.input, uri->host_kind() == c.kind);
12✔
134
         }
6✔
135

136
         const std::vector<std::string> invalid = {
1✔
137
            "",
138
            "://no.scheme/",
139
            "1http://host/",
140
            "https//no-colon/",
141
            "https://",
142
            "https:///path",
143
            "https://[not-an-ip]/",
144
            "https://example.com:0443/",
145
            // URIs without an authority are valid per RFC 5280 but seem unnecessary to support
146
            "urn:ashes",
147
            "mailto:root@attacker.com",
148
            "tel:867-5309",
149
            // Path/query/fragment must use RFC 3986 character set.
150
            "https://example.com/has space",
151
            "https://example.com/path<bracket>",
152
            "https://example.com/%G0",
153
            "https://example.com/%2",
154
            // Percent encoded embedded nulls are rejected
155
            "https://example.com/embedded/null/%00/surprise",
156
            // Fragment delimiter may appear at most once
157
            "https://example.com/path#frag#extra",
158
            "https://example.com/#a#b",
159
            // RFC 3986 userinfo does not allow unencoded '@'.
160
            "https://user@bad@example.com/",
161
            // Userinfo character set validation:
162
            "https://user name@example.com/",
163
            "https://user<x>@example.com/",
164
            "https://user\xff@example.com/",
165
            "https://user%G0@example.com/",
166
            "https://user%00null@example.com/",
167
         };
1✔
168
         for(const auto& s : invalid) {
25✔
169
            result.test_is_false("rejects invalid URI '" + s + "'", Botan::URI::parse(s).has_value());
72✔
170
         }
171

172
         return result;
1✔
173
      }
3✔
174

175
      static Test::Result test_equality() {
1✔
176
         Test::Result result("URI equality semantics");
1✔
177

178
         // Two URIs that share scheme + host + port but differ in path
179
         // are distinct identities. Critical for SPIFFE-style workload
180
         // IDs encoded as URI SANs - otherwise `uri_names().contains()`
181
         // could be satisfied by the wrong workload identity.
182
         const auto a = Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value();
2✔
183
         const auto b = Botan::URI::parse("spiffe://trust.example/ns/prod/sa/server").value();
2✔
184
         result.test_is_false("SPIFFE: differing paths are not equal", a == b);
1✔
185
         result.test_is_true("SPIFFE: equal with the same path",
2✔
186
                             a == Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value());
2✔
187

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

193
         // Path case IS significant (RFC 3986 - paths are not
194
         // case-canonicalized).
195
         result.test_is_false("path case is significant",
2✔
196
                              Botan::URI::parse("https://example.com/Path").value() ==
3✔
197
                                 Botan::URI::parse("https://example.com/path").value());
2✔
198

199
         // Userinfo is preserved verbatim and participates in identity
200
         // (RFC 3986 6.2 case-normalizes only scheme and host; RFC 5280
201
         // 7.4 requires URI comparison to be exact-match after that).
202
         result.test_is_false("userinfo distinguishes identity",
2✔
203
                              Botan::URI::parse("https://alice:s3cret@example.com/").value() ==
3✔
204
                                 Botan::URI::parse("https://example.com/").value());
2✔
205
         result.test_is_true("userinfo equal when matching",
2✔
206
                             Botan::URI::parse("https://alice:s3cret@example.com/").value() ==
3✔
207
                                Botan::URI::parse("https://alice:s3cret@example.com/").value());
2✔
208
         // The authority's original_input() includes the userinfo
209
         result.test_str_eq("authority original_input preserves userinfo",
2✔
210
                            Botan::URI::parse("https://alice:s3cret@example.com/").value().authority().original_input(),
2✔
211
                            "alice:s3cret@example.com");
212
         // Userinfo case IS significant (no case normalization).
213
         result.test_is_false("userinfo case is significant",
2✔
214
                              Botan::URI::parse("https://Alice@example.com/").value() ==
3✔
215
                                 Botan::URI::parse("https://alice@example.com/").value());
2✔
216
         // Empty userinfo is distinct from no userinfo (RFC 3986
217
         // authority grammar: "@" delimiter presence is significant).
218
         const auto absent = Botan::URI::parse("https://example.com/").value();
2✔
219
         const auto empty = Botan::URI::parse("https://@example.com/").value();
2✔
220
         result.test_is_false("empty userinfo != absent userinfo", absent == empty);
1✔
221
         result.test_is_false("absent userinfo: accessor reports nullopt", absent.authority().userinfo().has_value());
1✔
222
         result.test_is_true("empty userinfo: accessor reports present", empty.authority().userinfo().has_value());
1✔
223
         result.test_str_eq("empty userinfo: accessor reports empty string", *empty.authority().userinfo(), "");
1✔
224

225
         // Path, query, and fragment are split out and exposed separately.
226
         const auto full = Botan::URI::parse("https://example.com/path?q=1#frag").value();
2✔
227
         result.test_str_eq("path component", full.path(), "/path");
1✔
228
         result.test_is_true("query present", full.query().has_value());
1✔
229
         result.test_str_eq("query component", *full.query(), "q=1");
1✔
230
         result.test_is_true("fragment present", full.fragment().has_value());
1✔
231
         result.test_str_eq("fragment component", *full.fragment(), "frag");
1✔
232

233
         // Empty path is preserved as empty (not defaulted to "/").
234
         const auto no_path = Botan::URI::parse("https://example.com").value();
2✔
235
         result.test_str_eq("empty path stays empty", no_path.path(), "");
1✔
236
         result.test_is_false("no query", no_path.query().has_value());
1✔
237
         result.test_is_false("no fragment", no_path.fragment().has_value());
1✔
238

239
         // Query without path: empty path, present query.
240
         const auto query_only = Botan::URI::parse("https://example.com?q=1").value();
2✔
241
         result.test_str_eq("query-only: path is empty", query_only.path(), "");
1✔
242
         result.test_is_true("query-only: query present", query_only.query().has_value());
1✔
243
         result.test_str_eq("query-only: query value", *query_only.query(), "q=1");
1✔
244

245
         // Present-but-empty query / fragment are distinct from absent.
246
         const auto empty_query = Botan::URI::parse("https://example.com/p?").value();
2✔
247
         result.test_is_true("empty query: present", empty_query.query().has_value());
1✔
248
         result.test_str_eq("empty query: value", *empty_query.query(), "");
1✔
249
         const auto empty_frag = Botan::URI::parse("https://example.com/p#").value();
2✔
250
         result.test_is_true("empty fragment: present", empty_frag.fragment().has_value());
1✔
251
         result.test_str_eq("empty fragment: value", *empty_frag.fragment(), "");
1✔
252

253
         return result;
1✔
254
      }
1✔
255

256
   public:
257
      std::vector<Test::Result> run() override { return {test_authority_parse(), test_parse(), test_equality()}; }
4✔
258
};
259

260
BOTAN_REGISTER_TEST("utils", "uri", URI_Tests);
261

262
}  // namespace
263

264
#endif
265

266
}  // 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