1 // Copyright (c) 2017 Cloudflare, Inc. and contributors
2 // Licensed under the MIT License:
3 //
4 // Permission is hereby granted, free of charge, to any person obtaining a copy
5 // of this software and associated documentation files (the "Software"), to deal
6 // in the Software without restriction, including without limitation the rights
7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 // copies of the Software, and to permit persons to whom the Software is
9 // furnished to do so, subject to the following conditions:
10 //
11 // The above copyright notice and this permission notice shall be included in
12 // all copies or substantial portions of the Software.
13 //
14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 // THE SOFTWARE.
21
22 #include "url.h"
23 #include <kj/debug.h>
24 #include <kj/test.h>
25
26 namespace kj {
27 namespace {
28
parseAndCheck(kj::StringPtr originalText,kj::StringPtr expectedRestringified=nullptr,Url::Options options={})29 Url parseAndCheck(kj::StringPtr originalText, kj::StringPtr expectedRestringified = nullptr,
30 Url::Options options = {}) {
31 if (expectedRestringified == nullptr) expectedRestringified = originalText;
32 auto url = Url::parse(originalText, Url::REMOTE_HREF, options);
33 KJ_EXPECT(kj::str(url) == expectedRestringified, url, originalText, expectedRestringified);
34 // Make sure clones also restringify to the expected string.
35 auto clone = url.clone();
36 KJ_EXPECT(kj::str(clone) == expectedRestringified, clone, originalText, expectedRestringified);
37 return url;
38 }
39
40 static constexpr Url::Options NO_DECODE {
41 false, // percentDecode
42 false, // allowEmpty
43 };
44
45 static constexpr Url::Options ALLOW_EMPTY {
46 true, // percentDecode
47 true, // allowEmpty
48 };
49
50 KJ_TEST("parse / stringify URL") {
51 {
52 auto url = parseAndCheck("https://capnproto.org");
53 KJ_EXPECT(url.scheme == "https");
54 KJ_EXPECT(url.userInfo == nullptr);
55 KJ_EXPECT(url.host == "capnproto.org");
56 KJ_EXPECT(url.path == nullptr);
57 KJ_EXPECT(!url.hasTrailingSlash);
58 KJ_EXPECT(url.query == nullptr);
59 KJ_EXPECT(url.fragment == nullptr);
60 }
61
62 {
63 auto url = parseAndCheck("https://capnproto.org:80");
64 KJ_EXPECT(url.scheme == "https");
65 KJ_EXPECT(url.userInfo == nullptr);
66 KJ_EXPECT(url.host == "capnproto.org:80");
67 KJ_EXPECT(url.path == nullptr);
68 KJ_EXPECT(!url.hasTrailingSlash);
69 KJ_EXPECT(url.query == nullptr);
70 KJ_EXPECT(url.fragment == nullptr);
71 }
72
73 {
74 auto url = parseAndCheck("https://capnproto.org/");
75 KJ_EXPECT(url.scheme == "https");
76 KJ_EXPECT(url.userInfo == nullptr);
77 KJ_EXPECT(url.host == "capnproto.org");
78 KJ_EXPECT(url.path == nullptr);
79 KJ_EXPECT(url.hasTrailingSlash);
80 KJ_EXPECT(url.query == nullptr);
81 KJ_EXPECT(url.fragment == nullptr);
82 }
83
84 {
85 auto url = parseAndCheck("https://capnproto.org/foo/bar");
86 KJ_EXPECT(url.scheme == "https");
87 KJ_EXPECT(url.userInfo == nullptr);
88 KJ_EXPECT(url.host == "capnproto.org");
89 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
90 KJ_EXPECT(!url.hasTrailingSlash);
91 KJ_EXPECT(url.query == nullptr);
92 KJ_EXPECT(url.fragment == nullptr);
93 }
94
95 {
96 auto url = parseAndCheck("https://capnproto.org/foo/bar/");
97 KJ_EXPECT(url.scheme == "https");
98 KJ_EXPECT(url.userInfo == nullptr);
99 KJ_EXPECT(url.host == "capnproto.org");
100 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
101 KJ_EXPECT(url.hasTrailingSlash);
102 KJ_EXPECT(url.query == nullptr);
103 KJ_EXPECT(url.fragment == nullptr);
104 }
105
106 {
107 auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux&corge#garply");
108 KJ_EXPECT(url.scheme == "https");
109 KJ_EXPECT(url.userInfo == nullptr);
110 KJ_EXPECT(url.host == "capnproto.org");
111 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
112 KJ_EXPECT(!url.hasTrailingSlash);
113 KJ_ASSERT(url.query.size() == 2);
114 KJ_EXPECT(url.query[0].name == "baz");
115 KJ_EXPECT(url.query[0].value == "qux");
116 KJ_EXPECT(url.query[1].name == "corge");
117 KJ_EXPECT(url.query[1].value == nullptr);
118 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
119 }
120
121 {
122 auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux&corge=#garply");
123 KJ_EXPECT(url.scheme == "https");
124 KJ_EXPECT(url.userInfo == nullptr);
125 KJ_EXPECT(url.host == "capnproto.org");
126 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
127 KJ_EXPECT(!url.hasTrailingSlash);
128 KJ_ASSERT(url.query.size() == 2);
129 KJ_EXPECT(url.query[0].name == "baz");
130 KJ_EXPECT(url.query[0].value == "qux");
131 KJ_EXPECT(url.query[1].name == "corge");
132 KJ_EXPECT(url.query[1].value == nullptr);
133 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
134 }
135
136 {
137 auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=&corge=grault#garply");
138 KJ_EXPECT(url.scheme == "https");
139 KJ_EXPECT(url.userInfo == nullptr);
140 KJ_EXPECT(url.host == "capnproto.org");
141 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
142 KJ_EXPECT(!url.hasTrailingSlash);
143 KJ_ASSERT(url.query.size() == 2);
144 KJ_EXPECT(url.query[0].name == "baz");
145 KJ_EXPECT(url.query[0].value == "");
146 KJ_EXPECT(url.query[1].name == "corge");
147 KJ_EXPECT(url.query[1].value == "grault");
148 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
149 }
150
151 {
152 auto url = parseAndCheck("https://capnproto.org/foo/bar/?baz=qux&corge=grault#garply");
153 KJ_EXPECT(url.scheme == "https");
154 KJ_EXPECT(url.userInfo == nullptr);
155 KJ_EXPECT(url.host == "capnproto.org");
156 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
157 KJ_EXPECT(url.hasTrailingSlash);
158 KJ_ASSERT(url.query.size() == 2);
159 KJ_EXPECT(url.query[0].name == "baz");
160 KJ_EXPECT(url.query[0].value == "qux");
161 KJ_EXPECT(url.query[1].name == "corge");
162 KJ_EXPECT(url.query[1].value == "grault");
163 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
164 }
165
166 {
167 auto url = parseAndCheck("https://capnproto.org/foo/bar?baz=qux#garply");
168 KJ_EXPECT(url.scheme == "https");
169 KJ_EXPECT(url.userInfo == nullptr);
170 KJ_EXPECT(url.host == "capnproto.org");
171 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
172 KJ_EXPECT(!url.hasTrailingSlash);
173 KJ_ASSERT(url.query.size() == 1);
174 KJ_EXPECT(url.query[0].name == "baz");
175 KJ_EXPECT(url.query[0].value == "qux");
176 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
177 }
178
179 {
180 auto url = parseAndCheck("https://capnproto.org/foo?bar%20baz=qux+quux",
181 "https://capnproto.org/foo?bar+baz=qux+quux");
182 KJ_ASSERT(url.query.size() == 1);
183 KJ_EXPECT(url.query[0].name == "bar baz");
184 KJ_EXPECT(url.query[0].value == "qux quux");
185 }
186
187 {
188 auto url = parseAndCheck("https://capnproto.org/foo/bar#garply");
189 KJ_EXPECT(url.scheme == "https");
190 KJ_EXPECT(url.userInfo == nullptr);
191 KJ_EXPECT(url.host == "capnproto.org");
192 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
193 KJ_EXPECT(!url.hasTrailingSlash);
194 KJ_EXPECT(url.query == nullptr);
195 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
196 }
197
198 {
199 auto url = parseAndCheck("https://capnproto.org/foo/bar/#garply");
200 KJ_EXPECT(url.scheme == "https");
201 KJ_EXPECT(url.userInfo == nullptr);
202 KJ_EXPECT(url.host == "capnproto.org");
203 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
204 KJ_EXPECT(url.hasTrailingSlash);
205 KJ_EXPECT(url.query == nullptr);
206 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
207 }
208
209 {
210 auto url = parseAndCheck("https://capnproto.org#garply");
211 KJ_EXPECT(url.scheme == "https");
212 KJ_EXPECT(url.userInfo == nullptr);
213 KJ_EXPECT(url.host == "capnproto.org");
214 KJ_EXPECT(url.path == nullptr);
215 KJ_EXPECT(!url.hasTrailingSlash);
216 KJ_EXPECT(url.query == nullptr);
217 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
218 }
219
220 {
221 auto url = parseAndCheck("https://capnproto.org/#garply");
222 KJ_EXPECT(url.scheme == "https");
223 KJ_EXPECT(url.userInfo == nullptr);
224 KJ_EXPECT(url.host == "capnproto.org");
225 KJ_EXPECT(url.path == nullptr);
226 KJ_EXPECT(url.hasTrailingSlash);
227 KJ_EXPECT(url.query == nullptr);
228 KJ_EXPECT(KJ_ASSERT_NONNULL(url.fragment) == "garply");
229 }
230
231 {
232 auto url = parseAndCheck("https://foo@capnproto.org");
233 KJ_EXPECT(url.scheme == "https");
234 auto& user = KJ_ASSERT_NONNULL(url.userInfo);
235 KJ_EXPECT(user.username == "foo");
236 KJ_EXPECT(user.password == nullptr);
237 KJ_EXPECT(url.host == "capnproto.org");
238 KJ_EXPECT(url.path == nullptr);
239 KJ_EXPECT(!url.hasTrailingSlash);
240 KJ_EXPECT(url.query == nullptr);
241 KJ_EXPECT(url.fragment == nullptr);
242 }
243
244 {
245 auto url = parseAndCheck("https://$foo&:12+,34@capnproto.org");
246 KJ_EXPECT(url.scheme == "https");
247 auto& user = KJ_ASSERT_NONNULL(url.userInfo);
248 KJ_EXPECT(user.username == "$foo&");
249 KJ_EXPECT(KJ_ASSERT_NONNULL(user.password) == "12+,34");
250 KJ_EXPECT(url.host == "capnproto.org");
251 KJ_EXPECT(url.path == nullptr);
252 KJ_EXPECT(!url.hasTrailingSlash);
253 KJ_EXPECT(url.query == nullptr);
254 KJ_EXPECT(url.fragment == nullptr);
255 }
256
257 {
258 auto url = parseAndCheck("https://[2001:db8::1234]:80/foo");
259 KJ_EXPECT(url.scheme == "https");
260 KJ_EXPECT(url.userInfo == nullptr);
261 KJ_EXPECT(url.host == "[2001:db8::1234]:80");
262 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo"}));
263 KJ_EXPECT(!url.hasTrailingSlash);
264 KJ_EXPECT(url.query == nullptr);
265 KJ_EXPECT(url.fragment == nullptr);
266 }
267
268 {
269 auto url = parseAndCheck("https://capnproto.org/foo%2Fbar/baz");
270 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo/bar", "baz"}));
271 }
272
273 parseAndCheck("https://capnproto.org/foo/bar?", "https://capnproto.org/foo/bar");
274 parseAndCheck("https://capnproto.org/foo/bar?#", "https://capnproto.org/foo/bar#");
275 parseAndCheck("https://capnproto.org/foo/bar#");
276
277 // Scheme and host are forced to lower-case.
278 parseAndCheck("hTtP://capNprotO.org/fOo/bAr", "http://capnproto.org/fOo/bAr");
279
280 // URLs with underscores in their hostnames are allowed, but you probably shouldn't use them. They
281 // are not valid domain names.
282 parseAndCheck("https://bad_domain.capnproto.org/");
283
284 // Make sure URLs with %-encoded '%' signs in their userinfo, path, query, and fragment components
285 // get correctly re-encoded.
286 parseAndCheck("https://foo%25bar:baz%25qux@capnproto.org/");
287 parseAndCheck("https://capnproto.org/foo%25bar");
288 parseAndCheck("https://capnproto.org/?foo%25bar=baz%25qux");
289 parseAndCheck("https://capnproto.org/#foo%25bar");
290
291 // Make sure redundant /'s and &'s aren't collapsed when options.removeEmpty is false.
292 parseAndCheck("https://capnproto.org/foo//bar///test//?foo=bar&&baz=qux&", nullptr, ALLOW_EMPTY);
293
294 // "." and ".." are still processed, though.
295 parseAndCheck("https://capnproto.org/foo//../bar/.",
296 "https://capnproto.org/foo/bar/", ALLOW_EMPTY);
297
298 {
299 auto url = parseAndCheck("https://foo/", nullptr, ALLOW_EMPTY);
300 KJ_EXPECT(url.path.size() == 0);
301 KJ_EXPECT(url.hasTrailingSlash);
302 }
303
304 {
305 auto url = parseAndCheck("https://foo/bar/", nullptr, ALLOW_EMPTY);
306 KJ_EXPECT(url.path.size() == 1);
307 KJ_EXPECT(url.hasTrailingSlash);
308 }
309 }
310
311 KJ_TEST("URL percent encoding") {
312 parseAndCheck(
313 "https://b%6fb:%61bcd@capnpr%6fto.org/f%6fo?b%61r=b%61z#q%75x",
314 "https://bob:abcd@capnproto.org/foo?bar=baz#qux");
315
316 parseAndCheck(
317 "https://b\001b:\001bcd@capnproto.org/f\001o?b\001r=b\001z#q\001x",
318 "https://b%01b:%01bcd@capnproto.org/f%01o?b%01r=b%01z#q%01x");
319
320 parseAndCheck(
321 "https://b b: bcd@capnproto.org/f o?b r=b z#q x",
322 "https://b%20b:%20bcd@capnproto.org/f%20o?b+r=b+z#q%20x");
323
324 parseAndCheck(
325 "https://capnproto.org/foo?bar=baz#@?#^[\\]{|}",
326 "https://capnproto.org/foo?bar=baz#@?#^[\\]{|}");
327
328 // All permissible non-alphanumeric, non-separator path characters.
329 parseAndCheck(
330 "https://capnproto.org/!$&'()*+,-.:;=@[]^_|~",
331 "https://capnproto.org/!$&'()*+,-.:;=@[]^_|~");
332 }
333
334 KJ_TEST("parse / stringify URL w/o decoding") {
335 {
336 auto url = parseAndCheck("https://capnproto.org/foo%2Fbar/baz", nullptr, NO_DECODE);
337 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo%2Fbar", "baz"}));
338 }
339
340 {
341 // This case would throw an exception without NO_DECODE.
342 Url url = parseAndCheck("https://capnproto.org/R%20%26%20S?%foo=%QQ", nullptr, NO_DECODE);
343 KJ_EXPECT(url.scheme == "https");
344 KJ_EXPECT(url.host == "capnproto.org");
345 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"R%20%26%20S"}));
346 KJ_EXPECT(!url.hasTrailingSlash);
347 KJ_ASSERT(url.query.size() == 1);
348 KJ_EXPECT(url.query[0].name == "%foo");
349 KJ_EXPECT(url.query[0].value == "%QQ");
350 }
351 }
352
353 KJ_TEST("URL relative paths") {
354 parseAndCheck(
355 "https://capnproto.org/foo//bar",
356 "https://capnproto.org/foo/bar");
357
358 parseAndCheck(
359 "https://capnproto.org/foo/./bar",
360 "https://capnproto.org/foo/bar");
361
362 parseAndCheck(
363 "https://capnproto.org/foo/bar//",
364 "https://capnproto.org/foo/bar/");
365
366 parseAndCheck(
367 "https://capnproto.org/foo/bar/.",
368 "https://capnproto.org/foo/bar/");
369
370 parseAndCheck(
371 "https://capnproto.org/foo/baz/../bar",
372 "https://capnproto.org/foo/bar");
373
374 parseAndCheck(
375 "https://capnproto.org/foo/bar/baz/..",
376 "https://capnproto.org/foo/bar/");
377
378 parseAndCheck(
379 "https://capnproto.org/..",
380 "https://capnproto.org/");
381
382 parseAndCheck(
383 "https://capnproto.org/foo/../..",
384 "https://capnproto.org/");
385 }
386
387 KJ_TEST("URL for HTTP request") {
388 {
389 Url url = Url::parse("https://bob:1234@capnproto.org/foo/bar?baz=qux#corge");
390 KJ_EXPECT(url.toString(Url::REMOTE_HREF) ==
391 "https://bob:1234@capnproto.org/foo/bar?baz=qux#corge");
392 KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/foo/bar?baz=qux");
393 KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/foo/bar?baz=qux");
394 }
395
396 {
397 Url url = Url::parse("https://capnproto.org");
398 KJ_EXPECT(url.toString(Url::REMOTE_HREF) == "https://capnproto.org");
399 KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org");
400 KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/");
401 }
402
403 {
404 Url url = Url::parse("/foo/bar?baz=qux&corge", Url::HTTP_REQUEST);
405 KJ_EXPECT(url.scheme == nullptr);
406 KJ_EXPECT(url.host == nullptr);
407 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
408 KJ_EXPECT(!url.hasTrailingSlash);
409 KJ_ASSERT(url.query.size() == 2);
410 KJ_EXPECT(url.query[0].name == "baz");
411 KJ_EXPECT(url.query[0].value == "qux");
412 KJ_EXPECT(url.query[1].name == "corge");
413 KJ_EXPECT(url.query[1].value == nullptr);
414 }
415
416 {
417 Url url = Url::parse("https://capnproto.org/foo/bar?baz=qux&corge", Url::HTTP_PROXY_REQUEST);
418 KJ_EXPECT(url.scheme == "https");
419 KJ_EXPECT(url.host == "capnproto.org");
420 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>({"foo", "bar"}));
421 KJ_EXPECT(!url.hasTrailingSlash);
422 KJ_ASSERT(url.query.size() == 2);
423 KJ_EXPECT(url.query[0].name == "baz");
424 KJ_EXPECT(url.query[0].value == "qux");
425 KJ_EXPECT(url.query[1].name == "corge");
426 KJ_EXPECT(url.query[1].value == nullptr);
427 }
428
429 {
430 // '#' is allowed in path components in the HTTP_REQUEST context.
431 Url url = Url::parse("/foo#bar", Url::HTTP_REQUEST);
432 KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/foo%23bar");
433 KJ_EXPECT(url.scheme == nullptr);
434 KJ_EXPECT(url.host == nullptr);
435 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>{"foo#bar"});
436 KJ_EXPECT(!url.hasTrailingSlash);
437 KJ_EXPECT(url.query == nullptr);
438 KJ_EXPECT(url.fragment == nullptr);
439 }
440
441 {
442 // '#' is allowed in path components in the HTTP_PROXY_REQUEST context.
443 Url url = Url::parse("https://capnproto.org/foo#bar", Url::HTTP_PROXY_REQUEST);
444 KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/foo%23bar");
445 KJ_EXPECT(url.scheme == "https");
446 KJ_EXPECT(url.host == "capnproto.org");
447 KJ_EXPECT(url.path.asPtr() == kj::ArrayPtr<const StringPtr>{"foo#bar"});
448 KJ_EXPECT(!url.hasTrailingSlash);
449 KJ_EXPECT(url.query == nullptr);
450 KJ_EXPECT(url.fragment == nullptr);
451 }
452
453 {
454 // '#' is allowed in query components in the HTTP_REQUEST context.
455 Url url = Url::parse("/?foo=bar#123", Url::HTTP_REQUEST);
456 KJ_EXPECT(url.toString(Url::HTTP_REQUEST) == "/?foo=bar%23123");
457 KJ_EXPECT(url.scheme == nullptr);
458 KJ_EXPECT(url.host == nullptr);
459 KJ_EXPECT(url.path == nullptr);
460 KJ_EXPECT(url.hasTrailingSlash);
461 KJ_ASSERT(url.query.size() == 1);
462 KJ_EXPECT(url.query[0].name == "foo");
463 KJ_EXPECT(url.query[0].value == "bar#123");
464 KJ_EXPECT(url.fragment == nullptr);
465 }
466
467 {
468 // '#' is allowed in query components in the HTTP_PROXY_REQUEST context.
469 Url url = Url::parse("https://capnproto.org/?foo=bar#123", Url::HTTP_PROXY_REQUEST);
470 KJ_EXPECT(url.toString(Url::HTTP_PROXY_REQUEST) == "https://capnproto.org/?foo=bar%23123");
471 KJ_EXPECT(url.scheme == "https");
472 KJ_EXPECT(url.host == "capnproto.org");
473 KJ_EXPECT(url.path == nullptr);
474 KJ_EXPECT(url.hasTrailingSlash);
475 KJ_ASSERT(url.query.size() == 1);
476 KJ_EXPECT(url.query[0].name == "foo");
477 KJ_EXPECT(url.query[0].value == "bar#123");
478 KJ_EXPECT(url.fragment == nullptr);
479 }
480 }
481
482 KJ_TEST("parse URL failure") {
483 KJ_EXPECT(Url::tryParse("ht/tps://capnproto.org") == nullptr);
484 KJ_EXPECT(Url::tryParse("capnproto.org") == nullptr);
485 KJ_EXPECT(Url::tryParse("https:foo") == nullptr);
486
487 // percent-decode errors
488 KJ_EXPECT(Url::tryParse("https://capnproto.org/f%nno") == nullptr);
489 KJ_EXPECT(Url::tryParse("https://capnproto.org/foo?b%nnr=baz") == nullptr);
490
491 // components not valid in context
492 KJ_EXPECT(Url::tryParse("https://capnproto.org/foo", Url::HTTP_REQUEST) == nullptr);
493 KJ_EXPECT(Url::tryParse("https://bob:123@capnproto.org/foo", Url::HTTP_PROXY_REQUEST) == nullptr);
494 KJ_EXPECT(Url::tryParse("https://capnproto.org#/foo", Url::HTTP_PROXY_REQUEST) == nullptr);
495 }
496
parseAndCheckRelative(kj::StringPtr base,kj::StringPtr relative,kj::StringPtr expected,Url::Options options={})497 void parseAndCheckRelative(kj::StringPtr base, kj::StringPtr relative, kj::StringPtr expected,
498 Url::Options options = {}) {
499 auto parsed = Url::parse(base, Url::REMOTE_HREF, options).parseRelative(relative);
500 KJ_EXPECT(kj::str(parsed) == expected, parsed, expected);
501 auto clone = parsed.clone();
502 KJ_EXPECT(kj::str(clone) == expected, clone, expected);
503 }
504
505 KJ_TEST("parse relative URL") {
506 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
507 "#grault",
508 "https://capnproto.org/foo/bar?baz=qux#grault");
509 parseAndCheckRelative("https://capnproto.org/foo/bar?baz#corge",
510 "#grault",
511 "https://capnproto.org/foo/bar?baz#grault");
512 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=#corge",
513 "#grault",
514 "https://capnproto.org/foo/bar?baz=#grault");
515 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
516 "?grault",
517 "https://capnproto.org/foo/bar?grault");
518 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
519 "?grault=",
520 "https://capnproto.org/foo/bar?grault=");
521 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
522 "?grault+garply=waldo",
523 "https://capnproto.org/foo/bar?grault+garply=waldo");
524 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
525 "grault",
526 "https://capnproto.org/foo/grault");
527 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
528 "/grault",
529 "https://capnproto.org/grault");
530 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
531 "//grault",
532 "https://grault");
533 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
534 "//grault/garply",
535 "https://grault/garply");
536 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
537 "http:/grault",
538 "http://capnproto.org/grault");
539 parseAndCheckRelative("https://capnproto.org/foo/bar?baz=qux#corge",
540 "/http:/grault",
541 "https://capnproto.org/http:/grault");
542 parseAndCheckRelative("https://capnproto.org/",
543 "/foo/../bar",
544 "https://capnproto.org/bar");
545 }
546
547 KJ_TEST("parse relative URL w/o decoding") {
548 // This case would throw an exception without NO_DECODE.
549 parseAndCheckRelative("https://capnproto.org/R%20%26%20S?%foo=%QQ",
550 "%ANOTH%ERBAD%URL",
551 "https://capnproto.org/%ANOTH%ERBAD%URL", NO_DECODE);
552 }
553
554 KJ_TEST("parse relative URL failure") {
555 auto base = Url::parse("https://example.com/");
556 KJ_EXPECT(base.tryParseRelative("https://[not a host]") == nullptr);
557 }
558
559 } // namespace
560 } // namespace kj
561