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

randombit / botan / 26076563351

18 May 2026 04:08PM UTC coverage: 89.334% (+0.02%) from 89.316%
26076563351

push

github

web-flow
Merge pull request #5598 from randombit/jack/x509-nameconstraints-uri-email

Add URI and email name constraint processing

108478 of 121430 relevant lines covered (89.33%)

11163868.82 hits per line

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

96.39
/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,
13✔
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()) {
13✔
29
            result.test_failure(std::string(what) + ": port presence mismatch");
×
30
         } else if(got.has_value()) {
13✔
31
            result.test_u16_eq(what, *got, *expected);
6✔
32
         } else {
33
            result.test_success(std::string(what) + ": both nullopt");
21✔
34
         }
35
      }
20✔
36

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

40
         struct Case {
14✔
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", "0:0:0:0:0:0:0:1", 61234, HostKind::IPv6},
53
            {"[::1]", "0:0:0:0:0:0:0:1", std::nullopt, HostKind::IPv6},
54
            {"Example.COM:443", "example.com", 443, HostKind::DNS},
55
         };
8✔
56

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

68
         const std::vector<std::string> invalid = {
1✔
69
            "",
70
            "localhost::80",
71
            "localhost:80aa",
72
            "localhost:%50",
73
            "localhost:70000",
74
            "[::1]:a",
75
            "[::1]:70000",
76
            "hello..com",
77
            ".leading.dot",
78
            "[not-an-ipv6]:80",
79
            "[::1",
80
            "::1]:80",
81
            "host space:80",
82
            // Trailing dot is theoretically valid, but rejected
83
            "host.example.com.",
84
            "192.168.1.1.",
85
            "192.168.1.1.:8080",
86
            // _ not valid in host names, only DNS SRV records which is not relevant here
87
            "_acme-challenge.example.com",
88
         };
1✔
89
         for(const auto& s : invalid) {
18✔
90
            result.test_is_false("rejects invalid authority '" + s + "'", Botan::URI::Authority::parse(s).has_value());
51✔
91
         }
92

93
         return result;
1✔
94
      }
3✔
95

96
      static Test::Result test_parse() {
1✔
97
         Test::Result result("URI::parse");
1✔
98

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

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

116
         for(const auto& c : cases) {
7✔
117
            const auto uri = Botan::URI::parse(c.input);
6✔
118
            if(!result.test_is_true("parse succeeds: " + c.input, uri.has_value())) {
6✔
119
               continue;
×
120
            }
121
            result.test_str_eq("scheme: " + c.input, uri->scheme(), c.scheme);
6✔
122
            result.test_str_eq("host: " + c.input, uri->host_to_string(), c.host);
12✔
123
            test_port(result, "port: " + c.input, uri->port(), c.port);
6✔
124
            result.test_is_true("host kind: " + c.input, uri->host_kind() == c.kind);
12✔
125
         }
6✔
126

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

162
         return result;
1✔
163
      }
4✔
164

165
      static Test::Result test_equality() {
1✔
166
         Test::Result result("URI equality semantics");
1✔
167

168
         // Two URIs that share scheme + host + port but differ in path
169
         // are distinct identities. Critical for SPIFFE-style workload
170
         // IDs encoded as URI SANs - otherwise `uri_names().contains()`
171
         // could be satisfied by the wrong workload identity.
172
         const auto a = Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value();
2✔
173
         const auto b = Botan::URI::parse("spiffe://trust.example/ns/prod/sa/server").value();
2✔
174
         result.test_is_false("SPIFFE: differing paths are not equal", a == b);
1✔
175
         result.test_is_true("SPIFFE: equal with the same path",
2✔
176
                             a == Botan::URI::parse("spiffe://trust.example/ns/dev/sa/attacker").value());
2✔
177

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

183
         // Path case IS significant (RFC 3986 - paths are not
184
         // case-canonicalized).
185
         result.test_is_false("path case is significant",
2✔
186
                              Botan::URI::parse("https://example.com/Path").value() ==
3✔
187
                                 Botan::URI::parse("https://example.com/path").value());
2✔
188

189
         // Userinfo is preserved verbatim and participates in identity
190
         // (RFC 3986 6.2 case-normalizes only scheme and host; RFC 5280
191
         // 7.4 requires URI comparison to be exact-match after that).
192
         result.test_is_false("userinfo distinguishes identity",
2✔
193
                              Botan::URI::parse("https://alice:s3cret@example.com/").value() ==
3✔
194
                                 Botan::URI::parse("https://example.com/").value());
2✔
195
         result.test_is_true("userinfo equal when matching",
2✔
196
                             Botan::URI::parse("https://alice:s3cret@example.com/").value() ==
3✔
197
                                Botan::URI::parse("https://alice:s3cret@example.com/").value());
2✔
198
         // Userinfo case IS significant (no case normalization).
199
         result.test_is_false("userinfo case is significant",
2✔
200
                              Botan::URI::parse("https://Alice@example.com/").value() ==
3✔
201
                                 Botan::URI::parse("https://alice@example.com/").value());
2✔
202
         // Empty userinfo is distinct from no userinfo (RFC 3986
203
         // authority grammar: "@" delimiter presence is significant).
204
         const auto absent = Botan::URI::parse("https://example.com/").value();
2✔
205
         const auto empty = Botan::URI::parse("https://@example.com/").value();
2✔
206
         result.test_is_false("empty userinfo != absent userinfo", absent == empty);
1✔
207
         result.test_is_false("absent userinfo: accessor reports nullopt", absent.authority().userinfo().has_value());
1✔
208
         result.test_is_true("empty userinfo: accessor reports present", empty.authority().userinfo().has_value());
1✔
209
         result.test_str_eq("empty userinfo: accessor reports empty string", *empty.authority().userinfo(), "");
1✔
210

211
         // path_query_fragment exposes the verbatim tail.
212
         const auto with_query = Botan::URI::parse("https://example.com/path?q=1#frag").value();
2✔
213
         result.test_str_eq("path_query_fragment preserved", with_query.path_query_fragment(), "/path?q=1#frag");
1✔
214

215
         return result;
1✔
216
      }
1✔
217

218
   public:
219
      std::vector<Test::Result> run() override { return {test_authority_parse(), test_parse(), test_equality()}; }
4✔
220
};
221

222
BOTAN_REGISTER_TEST("utils", "uri", URI_Tests);
223

224
}  // namespace
225

226
#endif
227

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