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 }