1 /**
2 	URL parsing routines.
3 
4 	Copyright: © 2012-2017 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.inet.url;
9 
10 public import vibe.core.path;
11 
12 import vibe.textfilter.urlencode;
13 import vibe.utils.string;
14 
15 import std.array;
16 import std.conv;
17 import std.exception;
18 import std.string;
19 import std.traits : isInstanceOf;
20 import std.ascii : isAlpha;
21 
22 
23 /**
24 	Represents a URL decomposed into its components.
25 */
26 struct URL {
27 @safe:
28 	private {
29 		string m_schema;
30 		InetPath m_path;
31 		string m_host;
32 		ushort m_port;
33 		string m_username;
34 		string m_password;
35 		string m_queryString;
36 		string m_anchor;
37 	}
38 
39 	/// Constructs a new URL object from its components.
40 	this(string schema, string host, ushort port, InetPath path) pure nothrow
41 	in {
42 		assert(isValidSchema(schema), "Invalid URL schema name: " ~ schema);
43 		assert(host.length == 0 || isValidHostName(host), "Invalid URL host name: " ~ host);
44 	}
45 	do {
46 		m_schema = schema;
47 		m_host = host;
48 		m_port = port;
49 		m_path = path;
50 	}
51 	/// ditto
52 	this(string schema, InetPath path) pure nothrow
53 	in { assert(isValidSchema(schema)); }
54 	do {
55 		this(schema, null, 0, path);
56 	}
57 	/// ditto
58 	this(string schema, string host, ushort port, PosixPath path) pure nothrow
59 	in {
60 		assert(isValidSchema(schema));
61 		assert(host.length == 0 || isValidHostName(host));
62 	}
63 	do {
64 		InetPath ip;
65 		try ip = cast(InetPath)path;
66 		catch (Exception e) assert(false, e.msg); // InetPath should be able to capture all paths
67 		this(schema, host, port, ip);
68 	}
69 	/// ditto
70 	this(string schema, PosixPath path) pure nothrow
71 	in { assert(isValidSchema(schema)); }
72 	do {
73 		this(schema, null, 0, path);
74 	}
75 	/// ditto
76 	this(string schema, string host, ushort port, WindowsPath path) pure nothrow
77 	in {
78 		assert(isValidSchema(schema));
79 		assert(host.length == 0 || isValidHostName(host));
80 	}
81 	do {
82 		InetPath ip;
83 		try ip = cast(InetPath)path;
84 		catch (Exception e) assert(false, e.msg); // InetPath should be able to capture all paths
85 		this(schema, host, port, ip);
86 	}
87 	/// ditto
88 	this(string schema, WindowsPath path) pure nothrow
89 	in { assert(isValidSchema(schema)); }
90 	do {
91 		this(schema, null, 0, path);
92 	}
93 
94 	/** Constructs a "file:" URL from a native file system path.
95 
96 		Note that the path must be absolute. On Windows, both, paths starting
97 		with a drive letter and UNC paths are supported.
98 	*/
99 	this(WindowsPath path) pure
100 	{
101 		import std.algorithm.iteration : map;
102 		import std.range : chain, only, repeat;
103 
104 		enforce(path.absolute, "Only absolute paths can be converted to a URL.");
105 
106 		// treat UNC paths properly
107 		if (path.startsWith(WindowsPath(`\\`))) {
108 			static if (is(InetPath.Segment2)) {
109 				auto segs = path.bySegment2;
110 			} else {
111 				auto segs = path.bySegment;
112 			}
113 			segs.popFront();
114 			segs.popFront();
115 			auto host = segs.front.name;
116 			segs.popFront();
117 
118 			InetPath ip;
119 			static if (is(InetPath.Segment2)) {
120 				ip = InetPath(only(InetPath.Segment2.fromTrustedString("", '/'))
121 					.chain(segs.map!(s => cast(InetPath.Segment2)s)));
122 			} else {
123 				ip = InetPath(only(InetPath.Segment("", '/'))
124 					.chain(segs.map!(s => cast(InetPath.Segment)s)));
125 			}
126 
127 			this("file", host, 0, ip);
128 		} else this("file", host, 0, cast(InetPath)path);
129 	}
130 	/// ditto
131 	this(PosixPath path) pure
132 	{
133 		enforce(path.absolute, "Only absolute paths can be converted to a URL.");
134 
135 		this("file", null, 0, cast(InetPath)path);
136 	}
137 
138 	/** Constructs a URL from its string representation.
139 
140 		TODO: additional validation required (e.g. valid host and user names and port)
141 	*/
142 	this(string url_string)
143 	{
144 		auto str = url_string;
145 		enforce(str.length > 0, "Empty URL.");
146 		if( str[0] != '/' ){
147 			auto idx = str.indexOf(':');
148 			enforce(idx > 0, "No schema in URL:"~str);
149 			m_schema = str[0 .. idx];
150 			enforce(m_schema[0].isAlpha,
151 					"Schema must start with an alphabetical char, found: " ~
152 					m_schema[0]);
153 			str = str[idx+1 .. $];
154 			bool requires_host = false;
155 
156 			if (isCommonInternetSchema(m_schema)) {
157 				// proto://server/path style
158 				enforce(str.startsWith("//"), "URL must start with proto://...");
159 				requires_host = true;
160 				str = str[2 .. $];
161 			}
162 
163 			auto si = str.indexOf('/');
164 			if( si < 0 ) si = str.length;
165 			auto ai = str[0 .. si].indexOf('@');
166 			sizediff_t hs = 0;
167 			if( ai >= 0 ){
168 				hs = ai+1;
169 				auto ci = str[0 .. ai].indexOf(':');
170 				if( ci >= 0 ){
171 					m_username = str[0 .. ci];
172 					m_password = str[ci+1 .. ai];
173 				} else m_username = str[0 .. ai];
174 				enforce(m_username.length > 0, "Empty user name in URL.");
175 			}
176 
177 			m_host = str[hs .. si];
178 
179 			auto findPort ( string src )
180 			{
181 				auto pi = src.indexOf(':');
182 				if(pi > 0) {
183 					enforce(pi < src.length-1, "Empty port in URL.");
184 					m_port = to!ushort(src[pi+1..$]);
185 				}
186 				return pi;
187 			}
188 
189 
190 			auto ip6 = m_host.indexOf('[');
191 			if (ip6 == 0) { // [ must be first char
192 				auto pe = m_host.indexOf(']');
193 				if (pe > 0) {
194 					findPort(m_host[pe..$]);
195 					m_host = m_host[1 .. pe];
196 				}
197 			}
198 			else {
199 				auto pi = findPort(m_host);
200 				if(pi > 0) {
201 					m_host = m_host[0 .. pi];
202 				}
203 			}
204 
205 			enforce(!requires_host || m_schema == "file" || m_host.length > 0,
206 					"Empty server name in URL.");
207 			str = str[si .. $];
208 		}
209 
210 		this.localURI = str;
211 	}
212 	/// ditto
213 	static URL parse(string url_string)
214 	{
215 		return URL(url_string);
216 	}
217 	/// ditto
218 	static URL fromString(string url_string)
219 	{
220 		return URL(url_string);
221 	}
222 
223 	/// The schema/protocol part of the URL
224 	@property string schema() const nothrow { return m_schema; }
225 	/// ditto
226 	@property void schema(string v) { m_schema = v; }
227 
228 	/// The url encoded path part of the URL
229 	@property string pathString() const nothrow { return m_path.toString; }
230 
231 	/// Set the path part of the URL. It should be properly encoded.
232 	@property void pathString(string s)
233 	{
234 		enforce(isURLEncoded(s), "Wrong URL encoding of the path string '"~s~"'");
235 		m_path = InetPath(s);
236 	}
237 
238 	/// The path part of the URL
239 	@property InetPath path() const nothrow { return m_path; }
240 	/// ditto
241 	@property void path(InetPath p)
242 	nothrow {
243 		m_path = p;
244 	}
245 	/// ditto
246 	@property void path(Path)(Path p)
247 		if (isInstanceOf!(GenericPath, Path) && !is(Path == InetPath))
248 	{
249 		m_path = cast(InetPath)p;
250 	}
251 
252 	/// The host part of the URL (depends on the schema)
253 	@property string host() const pure nothrow { return m_host; }
254 	/// ditto
255 	@property void host(string v) { m_host = v; }
256 
257 	/// The port part of the URL (optional)
258 	@property ushort port() const nothrow { return m_port ? m_port : defaultPort(m_schema); }
259 	/// ditto
260 	@property port(ushort v) nothrow { m_port = v; }
261 
262 	/// Get the default port for the given schema or 0
263 	static ushort defaultPort(string schema)
264 	nothrow {
265 		switch (schema) {
266 			default:
267 			case "file": return 0;
268 			case "http": return 80;
269 			case "https": return 443;
270 			case "ftp": return 21;
271 			case "spdy": return 443;
272 			case "sftp": return 22;
273 		}
274 	}
275 	/// ditto
276 	ushort defaultPort()
277 	const nothrow {
278 		return defaultPort(m_schema);
279 	}
280 
281 	/// The user name part of the URL (optional)
282 	@property string username() const nothrow { return m_username; }
283 	/// ditto
284 	@property void username(string v) { m_username = v; }
285 
286 	/// The password part of the URL (optional)
287 	@property string password() const nothrow { return m_password; }
288 	/// ditto
289 	@property void password(string v) { m_password = v; }
290 
291 	/// The query string part of the URL (optional)
292 	@property string queryString() const nothrow { return m_queryString; }
293 	/// ditto
294 	@property void queryString(string v) { m_queryString = v; }
295 
296 	/// The anchor part of the URL (optional)
297 	@property string anchor() const nothrow { return m_anchor; }
298 
299 	/// The path part plus query string and anchor
300 	@property string localURI()
301 	const nothrow {
302 		auto str = appender!string();
303 		str.put(m_path.toString);
304 		if( queryString.length ) {
305 			str.put("?");
306 			str.put(queryString);
307 		}
308 		if( anchor.length ) {
309 			str.put("#");
310 			str.put(anchor);
311 		}
312 		return str.data;
313 	}
314 	/// ditto
315 	@property void localURI(string str)
316 	{
317 		auto ai = str.indexOf('#');
318 		if( ai >= 0 ){
319 			m_anchor = str[ai+1 .. $];
320 			str = str[0 .. ai];
321 		} else m_anchor = null;
322 
323 		auto qi = str.indexOf('?');
324 		if( qi >= 0 ){
325 			m_queryString = str[qi+1 .. $];
326 			str = str[0 .. qi];
327 		} else m_queryString = null;
328 
329 		this.pathString = str;
330 	}
331 
332 	/// The URL to the parent path with query string and anchor stripped.
333 	@property URL parentURL()
334 	const {
335 		URL ret;
336 		ret.schema = schema;
337 		ret.host = host;
338 		ret.port = port;
339 		ret.username = username;
340 		ret.password = password;
341 		ret.path = path.parentPath;
342 		return ret;
343 	}
344 
345 	/// Converts this URL object to its string representation.
346 	string toString()
347 	const nothrow {
348 		import std.format;
349 		auto dst = appender!string();
350 		dst.put(schema);
351 		dst.put(":");
352 		if (isCommonInternetSchema(schema))
353 			dst.put("//");
354 		if (m_username.length || m_password.length) {
355 			dst.put(username);
356 			dst.put(':');
357 			dst.put(password);
358 			dst.put('@');
359 		}
360 
361 		import std.algorithm : canFind;
362 		auto ipv6 = host.canFind(":");
363 
364 		if ( ipv6 ) dst.put('[');
365 		dst.put(host);
366 		if ( ipv6 ) dst.put(']');
367 
368 		if (m_port > 0) {
369 			try formattedWrite(dst, ":%d", m_port);
370 			catch (Exception e) assert(false, e.msg);
371 		}
372 
373 		dst.put(localURI);
374 		return dst.data;
375 	}
376 
377 	/** Converts a "file" URL back to a native file system path.
378 	*/
379 	NativePath toNativePath()
380 	const {
381 		import std.algorithm.iteration : map;
382 		import std.range : dropOne;
383 
384 		enforce(this.schema == "file", "Only file:// URLs can be converted to a native path.");
385 
386 		version (Windows) {
387 			if (this.host.length) {
388 				static if (is(NativePath.Segment2)) {
389 					auto p = NativePath(this.path
390 							.bySegment2
391 							.dropOne
392 							.map!(s => cast(WindowsPath.Segment2)s)
393 						);
394 				} else {
395 					auto p = NativePath(this.path
396 							.bySegment
397 							.dropOne
398 							.map!(s => cast(WindowsPath.Segment)s)
399 						);
400 				}
401 				return NativePath.fromTrustedString(`\\`~this.host) ~ p;
402 			}
403 		}
404 
405 		return cast(NativePath)this.path;
406 	}
407 
408 	bool startsWith(const URL rhs)
409 	const nothrow {
410 		if( m_schema != rhs.m_schema ) return false;
411 		if( m_host != rhs.m_host ) return false;
412 		// FIXME: also consider user, port, querystring, anchor etc
413 		static if (is(InetPath.Segment2))
414 			return this.path.bySegment2.startsWith(rhs.path.bySegment2);
415 		else return this.path.bySegment.startsWith(rhs.path.bySegment);
416 	}
417 
418 	URL opBinary(string OP, Path)(Path rhs) const if (OP == "~" && isAnyPath!Path) { return URL(m_schema, m_host, m_port, this.path ~ rhs); }
419 	URL opBinary(string OP, Path)(Path.Segment rhs) const if (OP == "~" && isAnyPath!Path) { return URL(m_schema, m_host, m_port, this.path ~ rhs); }
420 	void opOpAssign(string OP, Path)(Path rhs) if (OP == "~" && isAnyPath!Path) { this.path = this.path ~ rhs; }
421 	void opOpAssign(string OP, Path)(Path.Segment rhs) if (OP == "~" && isAnyPath!Path) { this.path = this.path ~ rhs; }
422 	static if (is(InetPath.Segment2)) {
423 		URL opBinary(string OP, Path)(Path.Segment2 rhs) const if (OP == "~" && isAnyPath!Path) { return URL(m_schema, m_host, m_port, this.path ~ rhs); }
424 		void opOpAssign(string OP, Path)(Path.Segment2 rhs) if (OP == "~" && isAnyPath!Path) { this.path = this.path ~ rhs; }
425 	}
426 
427 	/// Tests two URLs for equality using '=='.
428 	bool opEquals(ref const URL rhs)
429 	const nothrow {
430 		if (m_schema != rhs.m_schema) return false;
431 		if (m_host != rhs.m_host) return false;
432 		if (m_path != rhs.m_path) return false;
433 		return true;
434 	}
435 	/// ditto
436 	bool opEquals(const URL other) const nothrow { return opEquals(other); }
437 
438 	int opCmp(ref const URL rhs) const nothrow {
439 		if (m_schema != rhs.m_schema) return m_schema.cmp(rhs.m_schema);
440 		if (m_host != rhs.m_host) return m_host.cmp(rhs.m_host);
441 		if (m_path != rhs.m_path) return cmp(m_path.toString, rhs.m_path.toString);
442 		return true;
443 	}
444 }
445 
446 bool isValidSchema(string schema)
447 @safe pure nothrow {
448 	if (schema.length < 1) return false;
449 
450 	foreach (char ch; schema) {
451 		switch (ch) {
452 			default: return false;
453 			case 'a': .. case 'z': break;
454 			case '0': .. case '9': break;
455 			case '+', '.', '-': break;
456 		}
457 	}
458 
459 	return true;
460 }
461 
462 unittest {
463 	assert(isValidSchema("http+ssh"));
464 	assert(isValidSchema("http"));
465 	assert(!isValidSchema("http/ssh"));
466 }
467 
468 
469 bool isValidHostName(string name)
470 @safe pure nothrow {
471 	import std.algorithm.iteration : splitter;
472 	import std.string : representation;
473 
474 	// According to RFC 1034
475 	if (name.length < 1) return false;
476 	if (name.length > 255) return false;
477 	foreach (seg; name.representation.splitter('.')) {
478 		if (seg.length < 1) return false;
479 		if (seg.length > 63) return false;
480 		if (seg[0] == '-') return false;
481 
482 		foreach (char ch; seg) {
483 			switch (ch) {
484 				default: return false;
485 				case 'a': .. case 'z': break;
486 				case 'A': .. case 'Z': break;
487 				case '0': .. case '9': break;
488 				case '-': break;
489 			}
490 		}
491 	}
492 	return true;
493 }
494 
495 unittest {
496 	assert(isValidHostName("foo"));
497 	assert(isValidHostName("foo-"));
498 	assert(isValidHostName("foo.bar"));
499 	assert(isValidHostName("foo.bar-baz"));
500 	assert(isValidHostName("foo1"));
501 	assert(!isValidHostName("-foo"));
502 }
503 
504 
505 private enum isAnyPath(P) = is(P == InetPath) || is(P == PosixPath) || is(P == WindowsPath);
506 
507 private shared immutable(StringSet)* st_commonInternetSchemas;
508 
509 
510 /** Adds the name of a schema to be treated as double-slash style.
511 
512 	See_also: `isCommonInternetSchema`, RFC 1738 Section 3.1
513 */
514 void registerCommonInternetSchema(string schema)
515 @trusted nothrow {
516 	import core.atomic : atomicLoad, cas;
517 
518 	while (true) {
519 		auto olds = atomicLoad(st_commonInternetSchemas);
520 		auto news = olds ? olds.dup : new StringSet;
521 		news.add(schema);
522 		static if (__VERSION__ < 2094) {
523 			// work around bogus shared violation error on earlier versions of Druntime
524 			if (cas(cast(shared(StringSet*)*)&st_commonInternetSchemas, cast(shared(StringSet)*)olds, cast(shared(StringSet)*)news))
525 				break;
526 		} else {
527 			if (cas(&st_commonInternetSchemas, olds, cast(immutable)news))
528 				break;
529 		}
530 	}
531 }
532 
533 
534 /** Determines whether an URL schema is double-slash based.
535 
536 	Double slash based schemas are of the form `schema://[host]/<path>`
537 	and are parsed differently compared to generic schemas, which are simply
538 	parsed as `schema:<path>`.
539 
540 	Built-in recognized double-slash schemas: ftp, http, https,
541 	http+unix, https+unix, spdy, sftp, ws, wss, file, redis, tcp,
542 	rtsp, rtsps
543 
544 	See_also: `registerCommonInternetSchema`, RFC 1738 Section 3.1
545 */
546 bool isCommonInternetSchema(string schema)
547 @safe nothrow @nogc {
548 	import core.atomic : atomicLoad;
549 
550 	switch (schema) {
551 		case "ftp", "http", "https", "http+unix", "https+unix":
552 		case "spdy", "sftp", "ws", "wss", "file", "redis", "tcp":
553 		case "rtsp", "rtsps":
554 			return true;
555 		default:
556 			auto set = atomicLoad(st_commonInternetSchemas);
557 			return set ? set.contains(schema) : false;
558 	}
559 }
560 
561 unittest {
562 	assert(isCommonInternetSchema("http"));
563 	assert(!isCommonInternetSchema("foobar"));
564 	registerCommonInternetSchema("foobar");
565 	assert(isCommonInternetSchema("foobar"));
566 }
567 
568 
569 private struct StringSet {
570 	bool[string] m_data;
571 
572 	void add(string str) @safe nothrow { m_data[str] = true; }
573 	bool contains(string str) const @safe nothrow @nogc { return !!(str in m_data); }
574 	StringSet* dup() const @safe nothrow {
575 		auto ret = new StringSet;
576 		foreach (k; m_data.byKey) ret.add(k);
577 		return ret;
578 	}
579 }
580 
581 
582 unittest { // IPv6
583 	auto urlstr = "http://[2003:46:1a7b:6c01:64b:80ff:fe80:8003]:8091/abc";
584 	auto url = URL.parse(urlstr);
585 	assert(url.schema == "http", url.schema);
586 	assert(url.host == "2003:46:1a7b:6c01:64b:80ff:fe80:8003", url.host);
587 	assert(url.port == 8091);
588 	assert(url.path == InetPath("/abc"), url.path.toString());
589 	assert(url.toString == urlstr);
590 
591 	url.host = "abcd:46:1a7b:6c01:64b:80ff:fe80:8abc";
592 	urlstr = "http://[abcd:46:1a7b:6c01:64b:80ff:fe80:8abc]:8091/abc";
593 	assert(url.toString == urlstr);
594 }
595 
596 
597 unittest {
598 	auto urlstr = "https://www.example.net/index.html";
599 	auto url = URL.parse(urlstr);
600 	assert(url.schema == "https", url.schema);
601 	assert(url.host == "www.example.net", url.host);
602 	assert(url.path == InetPath("/index.html"), url.path.toString());
603 	assert(url.port == 443);
604 	assert(url.toString == urlstr);
605 
606 	urlstr = "http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor";
607 	url = URL.parse(urlstr);
608 	assert(url.schema == "http", url.schema);
609 	assert(url.username == "jo.doe", url.username);
610 	assert(url.password == "password", url.password);
611 	assert(url.port == 4711, to!string(url.port));
612 	assert(url.host == "sub.www.example.net", url.host);
613 	assert(url.path.toString() == "/sub2/index.html", url.path.toString());
614 	assert(url.queryString == "query", url.queryString);
615 	assert(url.anchor == "anchor", url.anchor);
616 	assert(url.toString == urlstr);
617 }
618 
619 unittest { // issue #1044
620 	URL url = URL.parse("http://example.com/p?query#anchor");
621 	assert(url.schema == "http");
622 	assert(url.host == "example.com");
623 	assert(url.port == 80);
624 	assert(url.queryString == "query");
625 	assert(url.anchor == "anchor");
626 	assert(url.pathString == "/p");
627 	url.localURI = "/q";
628 	assert(url.schema == "http");
629 	assert(url.host == "example.com");
630 	assert(url.queryString == "");
631 	assert(url.anchor == "");
632 	assert(url.pathString == "/q");
633 	url.localURI = "/q?query";
634 	assert(url.schema == "http");
635 	assert(url.host == "example.com");
636 	assert(url.queryString == "query");
637 	assert(url.anchor == "");
638 	assert(url.pathString == "/q");
639 	url.localURI = "/q#anchor";
640 	assert(url.schema == "http");
641 	assert(url.host == "example.com");
642 	assert(url.queryString == "");
643 	assert(url.anchor == "anchor");
644 	assert(url.pathString == "/q");
645 }
646 
647 //websocket unittest
648 unittest {
649 	URL url = URL("ws://127.0.0.1:8080/echo");
650 	assert(url.host == "127.0.0.1");
651 	assert(url.port == 8080);
652 	assert(url.localURI == "/echo");
653 }
654 
655 //rtsp unittest
656 unittest {
657 	URL url = URL("rtsp://127.0.0.1:554/echo");
658 	assert(url.host == "127.0.0.1");
659 	assert(url.port == 554);
660 	assert(url.localURI == "/echo");
661 }
662 
663 unittest {
664 	auto p = PosixPath("/foo bar/boo oom/");
665 	URL url = URL("http", "example.com", 0, p); // constructor test
666 	assert(url.path == cast(InetPath)p);
667 	url.path = p;
668 	assert(url.path == cast(InetPath)p);					   // path assignement test
669 	assert(url.pathString == "/foo%20bar/boo%20oom/");
670 	assert(url.toString() == "http://example.com/foo%20bar/boo%20oom/");
671 	url.pathString = "/foo%20bar/boo%2foom/";
672 	assert(url.pathString == "/foo%20bar/boo%2foom/");
673 	assert(url.toString() == "http://example.com/foo%20bar/boo%2foom/");
674 }
675 
676 unittest {
677 	auto url = URL("http://example.com/some%2bpath");
678 	assert((cast(PosixPath)url.path).toString() == "/some+path", url.path.toString());
679 }
680 
681 unittest {
682 	assert(URL("file:///test").pathString == "/test");
683 	assert(URL("file:///test").port == 0);
684 	assert(URL("file:///test").path.toString() == "/test");
685 	assert(URL("file://test").host == "test");
686 	assert(URL("file://test").pathString() == "");
687 	assert(URL("file://./test").host == ".");
688 	assert(URL("file://./test").pathString == "/test");
689 	assert(URL("file://./test").path.toString() == "/test");
690 }
691 
692 unittest { // issue #1318
693 	try {
694 		URL("http://something/inval%id");
695 		assert(false, "Expected to throw an exception.");
696 	} catch (Exception e) {}
697 }
698 
699 unittest {
700 	assert(URL("http+unix://%2Fvar%2Frun%2Fdocker.sock").schema == "http+unix");
701 	assert(URL("https+unix://%2Fvar%2Frun%2Fdocker.sock").schema == "https+unix");
702 	assert(URL("http+unix://%2Fvar%2Frun%2Fdocker.sock").host == "%2Fvar%2Frun%2Fdocker.sock");
703 	assert(URL("http+unix://%2Fvar%2Frun%2Fdocker.sock").pathString == "");
704 	assert(URL("http+unix://%2Fvar%2Frun%2Fdocker.sock/container/json").pathString == "/container/json");
705 	auto url = URL("http+unix://%2Fvar%2Frun%2Fdocker.sock/container/json");
706 	assert(URL(url.toString()) == url);
707 }
708 
709 unittest {
710 	import vibe.data.serialization;
711 	static assert(isStringSerializable!URL);
712 }
713 
714 unittest { // issue #1732
715 	auto url = URL("tcp://0.0.0.0:1234");
716 	url.port = 4321;
717 	assert(url.toString == "tcp://0.0.0.0:4321", url.toString);
718 }
719 
720 unittest { // host name role in file:// URLs
721 	auto url = URL.parse("file:///foo/bar");
722 	assert(url.host == "");
723 	assert(url.path == InetPath("/foo/bar"));
724 	assert(url.toString() == "file:///foo/bar");
725 
726 	url = URL.parse("file://foo/bar/baz");
727 	assert(url.host == "foo");
728 	assert(url.path == InetPath("/bar/baz"));
729 	assert(url.toString() == "file://foo/bar/baz");
730 }
731 
732 unittest { // native path <-> URL conversion
733 	import std.exception : assertThrown;
734 
735 	auto url = URL(NativePath("/foo/bar"));
736 	assert(url.schema == "file");
737 	assert(url.host == "");
738 	assert(url.path == InetPath("/foo/bar"));
739 	assert(url.toNativePath == NativePath("/foo/bar"));
740 
741 	assertThrown(URL("http://example.org/").toNativePath);
742 	assertThrown(URL(NativePath("foo/bar")));
743 }
744 
745 version (Windows) unittest { // Windows drive letter paths
746 	auto url = URL(WindowsPath(`C:\foo`));
747 	assert(url.schema == "file");
748 	assert(url.host == "");
749 	assert(url.path == InetPath("/C:/foo"));
750 	auto p = url.toNativePath;
751 	p.normalize();
752 	assert(p == WindowsPath(`C:\foo`));
753 }
754 
755 version (Windows) unittest { // UNC paths
756 	auto url = URL(WindowsPath(`\\server\share\path`));
757 	assert(url.schema == "file");
758 	assert(url.host == "server");
759 	assert(url.path == InetPath("/share/path"));
760 
761 	auto p = url.toNativePath;
762 	p.normalize(); // convert slash to backslash if necessary
763 	assert(p == WindowsPath(`\\server\share\path`));
764 }