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