1 /** 2 Contains routines for high level path handling. 3 4 Copyright: © 2012-2019 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.core.path; 9 10 import std.algorithm.searching : commonPrefix, endsWith, startsWith; 11 import std.algorithm.comparison : equal, min; 12 import std.algorithm.iteration : map; 13 import std.exception : enforce; 14 import std.range : empty, front, popFront, popFrontExactly, takeExactly; 15 import std.range.primitives : ElementType, isInputRange, isOutputRange; 16 import std.traits : isInstanceOf; 17 18 19 /** Computes the relative path from `base_path` to this path. 20 21 Params: 22 path = The destination path 23 base_path = The path from which the relative path starts 24 25 See_also: `relativeToWeb` 26 */ 27 Path relativeTo(Path)(Path path, Path base_path) @safe 28 if (isInstanceOf!(GenericPath, Path)) 29 { 30 import std.array : array, replicate; 31 import std.range : chain, drop, take; 32 33 assert(base_path.absolute, "Base path must be absolute for relativeTo."); 34 assert(path.absolute, "Path must be absolute for relativeTo."); 35 36 if (is(Path == WindowsPath)) { // FIXME: this shouldn't be a special case here! 37 bool samePrefix(size_t n) 38 { 39 return path.bySegment2.map!(n => n.encodedName).take(n).equal(base_path.bySegment2.map!(n => n.encodedName).take(n)); 40 } 41 // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case 42 auto pref = path.bySegment2; 43 if (!pref.empty && pref.front.encodedName == "") { 44 pref.popFront(); 45 if (!pref.empty) { 46 // different drive? 47 if (pref.front.encodedName.endsWith(':') && !samePrefix(2)) 48 return path; 49 // different UNC path? 50 if (pref.front.encodedName == "" && !samePrefix(4)) 51 return path; 52 } 53 } 54 } 55 56 auto nodes = path.bySegment2; 57 auto base_nodes = base_path.bySegment2; 58 59 // skip and count common prefix 60 size_t base = 0; 61 while (!nodes.empty && !base_nodes.empty && equal(nodes.front.name, base_nodes.front.name)) { 62 nodes.popFront(); 63 base_nodes.popFront(); 64 base++; 65 } 66 67 enum up = Path.Segment2("..", Path.defaultSeparator); 68 auto ret = Path(base_nodes.map!(p => up).chain(nodes)); 69 if (path.endsWithSlash) { 70 if (ret.empty) return Path.fromTrustedString("." ~ path.toString()[$-1]); 71 else ret.endsWithSlash = true; 72 } 73 return ret; 74 } 75 76 /// 77 unittest { 78 import std.array : array; 79 import std.conv : to; 80 assert(PosixPath("/some/path").relativeTo(PosixPath("/")) == PosixPath("some/path")); 81 assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path/")) == PosixPath("../../path/")); 82 assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path")) == PosixPath("../../path/")); 83 84 assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("C:\\")) == WindowsPath("some\\path")); 85 assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); 86 assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); 87 88 assert(WindowsPath("\\\\server\\share\\some\\path").relativeTo(WindowsPath("\\\\server\\share\\")) == WindowsPath("some\\path")); 89 assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); 90 assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); 91 92 assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("D:\\")) == WindowsPath("C:\\some\\path")); 93 assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share")) == WindowsPath("C:\\some\\path\\")); 94 assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("\\\\server\\some\\path\\")); 95 assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("\\\\otherserver\\path")) == WindowsPath("\\\\server\\some\\path\\")); 96 assert(WindowsPath("\\some\\path\\").relativeTo(WindowsPath("\\other\\path")) == WindowsPath("..\\..\\some\\path\\")); 97 98 assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share\\path2")) == WindowsPath("..\\path1")); 99 assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); 100 assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server2\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); 101 } 102 103 unittest { 104 { 105 auto parentpath = "/path/to/parent"; 106 auto parentpathp = PosixPath(parentpath); 107 auto subpath = "/path/to/parent/sub/"; 108 auto subpathp = PosixPath(subpath); 109 auto subpath_rel = "sub/"; 110 assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); 111 auto subfile = "/path/to/parent/child"; 112 auto subfilep = PosixPath(subfile); 113 auto subfile_rel = "child"; 114 assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); 115 } 116 117 { // relative paths across Windows devices are not allowed 118 auto p1 = WindowsPath("\\\\server\\share"); assert(p1.absolute); 119 auto p2 = WindowsPath("\\\\server\\othershare"); assert(p2.absolute); 120 auto p3 = WindowsPath("\\\\otherserver\\share"); assert(p3.absolute); 121 auto p4 = WindowsPath("C:\\somepath"); assert(p4.absolute); 122 auto p5 = WindowsPath("C:\\someotherpath"); assert(p5.absolute); 123 auto p6 = WindowsPath("D:\\somepath"); assert(p6.absolute); 124 auto p7 = WindowsPath("\\\\server\\share\\path"); assert(p7.absolute); 125 auto p8 = WindowsPath("\\\\server\\share\\otherpath"); assert(p8.absolute); 126 assert(p4.relativeTo(p5) == WindowsPath("..\\somepath")); 127 assert(p4.relativeTo(p6) == WindowsPath("C:\\somepath")); 128 assert(p4.relativeTo(p1) == WindowsPath("C:\\somepath")); 129 assert(p1.relativeTo(p2) == WindowsPath("\\\\server\\share")); 130 assert(p1.relativeTo(p3) == WindowsPath("\\\\server\\share")); 131 assert(p1.relativeTo(p4) == WindowsPath("\\\\server\\share")); 132 assert(p7.relativeTo(p1) == WindowsPath("path")); 133 assert(p7.relativeTo(p8) == WindowsPath("..\\path")); 134 } 135 136 { // relative path, trailing slash 137 auto p1 = PosixPath("/some/path"); 138 auto p2 = PosixPath("/some/path/"); 139 assert(p1.relativeTo(p1).toString() == ""); 140 assert(p1.relativeTo(p2).toString() == ""); 141 assert(p2.relativeTo(p2).toString() == "./"); 142 } 143 } 144 145 nothrow unittest { 146 auto p1 = PosixPath.fromTrustedString("/foo/bar/baz"); 147 auto p2 = PosixPath.fromTrustedString("/foo/baz/bam"); 148 assert(p2.relativeTo(p1).toString == "../../baz/bam"); 149 } 150 151 152 /** Computes the relative path to this path from `base_path` using web path rules. 153 154 The difference to `relativeTo` is that a path not ending in a slash 155 will not be considered as a path to a directory and the parent path 156 will instead be used. 157 158 Params: 159 path = The destination path 160 base_path = The path from which the relative path starts 161 162 See_also: `relativeTo` 163 */ 164 Path relativeToWeb(Path)(Path path, Path base_path) @safe 165 if (isInstanceOf!(GenericPath, Path)) 166 { 167 if (!base_path.endsWithSlash) { 168 assert(base_path.absolute, "Base path must be absolute for relativeToWeb."); 169 if (base_path.hasParentPath) base_path = base_path.parentPath; 170 else base_path = Path("/"); 171 assert(base_path.absolute); 172 } 173 return path.relativeTo(base_path); 174 } 175 176 /// 177 /+unittest { 178 assert(InetPath("/some/path").relativeToWeb(InetPath("/")) == InetPath("some/path")); 179 assert(InetPath("/some/path/").relativeToWeb(InetPath("/some/other/path/")) == InetPath("../../path/")); 180 assert(InetPath("/some/path/").relativeToWeb(InetPath("/some/other/path")) == InetPath("../path/")); 181 }+/ 182 183 184 /** Converts a path to its system native string representation. 185 */ 186 string toNativeString(P)(P path) 187 { 188 return (cast(NativePath)path).toString(); 189 } 190 191 192 /// Represents a path on Windows operating systems. 193 alias WindowsPath = GenericPath!WindowsPathFormat; 194 195 /// Represents a path on Unix/Posix systems. 196 alias PosixPath = GenericPath!PosixPathFormat; 197 198 /// Represents a path as part of an URI. 199 alias InetPath = GenericPath!InetPathFormat; 200 201 /// The path type native to the target operating system. 202 version (Windows) alias NativePath = WindowsPath; 203 else alias NativePath = PosixPath; 204 205 deprecated("Use NativePath or one the specific path types instead.") 206 alias Path = NativePath; 207 deprecated("Use NativePath.Segment or one the specific path types instead.") 208 alias PathEntry = Path.Segment; 209 210 /// Provides a common interface to operate on paths of various kinds. 211 struct GenericPath(F) { 212 @safe: 213 alias Format = F; 214 215 /** A single path segment. 216 */ 217 static struct Segment { 218 @safe: 219 220 private { 221 string m_name; 222 char m_separator = 0; 223 } 224 225 /** Constructs a new path segment including an optional trailing 226 separator. 227 228 Params: 229 name = The raw (unencoded) name of the path segment 230 separator = Optional trailing path separator (e.g. `'/'`) 231 232 Throws: 233 A `PathValidationException` is thrown if the name contains 234 characters that are invalid for the path type. In particular, 235 any path separator characters may not be part of the name. 236 */ 237 this(string name, char separator = '\0') 238 { 239 import std.algorithm.searching : any; 240 241 enforce!PathValidationException(separator == '\0' || Format.isSeparator(separator), 242 "Invalid path separator."); 243 auto err = Format.validateDecodedSegment(name); 244 enforce!PathValidationException(err is null, err); 245 246 m_name = name; 247 m_separator = separator; 248 } 249 250 /** Constructs a path segment without performing validation. 251 252 Note that in debug builds, there are still assertions in place 253 that verify that the provided values are valid. 254 255 Params: 256 name = The raw (unencoded) name of the path segment 257 separator = Optional trailing path separator (e.g. `'/'`) 258 */ 259 static Segment fromTrustedString(string name, char separator = '\0') 260 nothrow @nogc pure { 261 import std.algorithm.searching : any; 262 assert(separator == '\0' || Format.isSeparator(separator)); 263 assert(Format.validateDecodedSegment(name) is null, "Invalid path segment."); 264 265 Segment ret; 266 ret.m_name = name; 267 ret.m_separator = separator; 268 return ret; 269 } 270 271 deprecated("Use the constructor instead.") 272 static Segment validateFilename(string name) 273 { 274 return Segment(name); 275 } 276 277 /// The (file/directory) name of the path segment. 278 @property string name() const nothrow @nogc { return m_name; } 279 /// The trailing separator (e.g. `'/'`) or `'\0'`. 280 @property char separator() const nothrow @nogc { return m_separator; } 281 /// ditto 282 @property void separator(char ch) { 283 enforce!PathValidationException(ch == '\0' || Format.isSeparator(ch), 284 "Character is not a valid path separator."); 285 m_separator = ch; 286 } 287 /// Returns `true` $(I iff) the segment has a trailing path separator. 288 @property bool hasSeparator() const nothrow @nogc { return m_separator != '\0'; } 289 290 deprecated("Use .name instead.") 291 string toString() const nothrow @nogc { return m_name; } 292 293 /** Converts the segment to another path type. 294 295 The segment name will be re-validated during the conversion. The 296 separator, if any, will be adopted or replaced by the default 297 separator of the target path type. 298 299 Throws: 300 A `PathValidationException` is thrown if the segment name cannot 301 be represented in the target path format. 302 */ 303 GenericPath!F.Segment opCast(T : GenericPath!F.Segment, F)() 304 { 305 char dsep = '\0'; 306 if (m_separator) { 307 if (F.isSeparator(m_separator)) dsep = m_separator; 308 else dsep = F.defaultSeparator; 309 } 310 return GenericPath!F.Segment(m_name, dsep); 311 } 312 313 /// Compares two path segment names 314 bool opEquals(Segment other) const nothrow @nogc { return this.name == other.name && this.hasSeparator == other.hasSeparator; } 315 /// ditto 316 bool opEquals(string name) const nothrow @nogc { return this.name == name; } 317 } 318 319 /** Represents a path as an forward range of `Segment`s. 320 */ 321 static struct PathRange { 322 import std.traits : ReturnType; 323 324 private { 325 string m_path; 326 Segment m_front; 327 } 328 329 private this(string path) 330 { 331 m_path = path; 332 if (m_path.length) { 333 auto ap = Format.getAbsolutePrefix(m_path); 334 if (ap.length && !Format.isSeparator(ap[0])) 335 m_front = Segment.fromTrustedString(null, '/'); 336 else readFront(); 337 } 338 } 339 340 @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment.init; } 341 342 @property PathRange save() { return this; } 343 344 @property Segment front() { return m_front; } 345 346 void popFront() 347 nothrow { 348 assert(m_front != Segment.init); 349 if (m_path.length) readFront(); 350 else m_front = Segment.init; 351 } 352 353 private void readFront() 354 nothrow { 355 import std.array : array; 356 357 auto n = Format.getFrontNode(m_path); 358 m_path = m_path[n.length .. $]; 359 360 char sep = '\0'; 361 if (Format.isSeparator(n[$-1])) { 362 sep = n[$-1]; 363 n = n[0 .. $-1]; 364 } 365 static if (is(typeof(Format.decodeSingleSegment(n)) == string)) 366 string ndec = Format.decodeSingleSegment(n); 367 else 368 string ndec = Format.decodeSingleSegment(n).array; 369 m_front = Segment.fromTrustedString(ndec, sep); 370 assert(m_front != Segment.init); 371 } 372 } 373 374 /** A single path segment. 375 */ 376 static struct Segment2 { 377 @safe: 378 379 private { 380 string m_encodedName; 381 char m_separator = 0; 382 } 383 384 /** Constructs a new path segment including an optional trailing 385 separator. 386 387 Params: 388 name = The raw (unencoded) name of the path segment 389 separator = Optional trailing path separator (e.g. `'/'`) 390 391 Throws: 392 A `PathValidationException` is thrown if the name contains 393 characters that are invalid for the path type. In particular, 394 any path separator characters may not be part of the name. 395 */ 396 this(string name, char separator = '\0') 397 { 398 import std.algorithm.searching : any; 399 400 enforce!PathValidationException(separator == '\0' || Format.isSeparator(separator), 401 "Invalid path separator."); 402 auto err = Format.validateDecodedSegment(name); 403 enforce!PathValidationException(err is null, err); 404 405 m_encodedName = Format.encodeSegment(name); 406 m_separator = separator; 407 } 408 409 /** Constructs a path segment without performing validation. 410 411 Note that in debug builds, there are still assertions in place 412 that verify that the provided values are valid. 413 414 Params: 415 name = The raw (unencoded) name of the path segment 416 separator = Optional trailing path separator (e.g. `'/'`) 417 */ 418 static Segment2 fromTrustedString(string name, char separator = '\0') 419 nothrow pure { 420 import std.algorithm.searching : any; 421 assert(separator == '\0' || Format.isSeparator(separator)); 422 assert(Format.validateDecodedSegment(name) is null, "Invalid path segment."); 423 return fromTrustedEncodedString(Format.encodeSegment(name), separator); 424 } 425 426 /** Constructs a path segment without performing validation. 427 428 Note that in debug builds, there are still assertions in place 429 that verify that the provided values are valid. 430 431 Params: 432 encoded_name = The encoded name of the path segment 433 separator = Optional trailing path separator (e.g. `'/'`) 434 */ 435 static Segment2 fromTrustedEncodedString(string encoded_name, char separator = '\0') 436 nothrow @nogc pure { 437 import std.algorithm.searching : any; 438 import std.utf : byCodeUnit; 439 440 assert(separator == '\0' || Format.isSeparator(separator)); 441 assert(!encoded_name.byCodeUnit.any!(c => Format.isSeparator(c))); 442 assert(Format.validatePath(encoded_name) is null, "Invalid path segment."); 443 444 Segment2 ret; 445 ret.m_encodedName = encoded_name; 446 ret.m_separator = separator; 447 return ret; 448 } 449 450 /** The (file/directory) name of the path segment. 451 452 Note: Depending on the path type, this may return a generic range 453 type instead of `string`. Use `name.to!string` in that 454 case if you need an actual `string`. 455 */ 456 @property auto name() const nothrow @nogc { return Format.decodeSingleSegment(m_encodedName); } 457 /// The encoded representation of the path segment name 458 @property string encodedName() const nothrow @nogc { return m_encodedName; } 459 /// The trailing separator (e.g. `'/'`) or `'\0'`. 460 @property char separator() const nothrow @nogc { return m_separator; } 461 /// ditto 462 @property void separator(char ch) { 463 enforce!PathValidationException(ch == '\0' || Format.isSeparator(ch), 464 "Character is not a valid path separator."); 465 m_separator = ch; 466 } 467 /// Returns `true` $(I iff) the segment has a trailing path separator. 468 @property bool hasSeparator() const nothrow @nogc { return m_separator != '\0'; } 469 470 /** Converts the segment to another path type. 471 472 The segment name will be re-validated during the conversion. The 473 separator, if any, will be adopted or replaced by the default 474 separator of the target path type. 475 476 Throws: 477 A `PathValidationException` is thrown if the segment name cannot 478 be represented in the target path format. 479 */ 480 GenericPath!F.Segment2 opCast(T : GenericPath!F.Segment2, F)() 481 { 482 import std.array : array; 483 484 char dsep = '\0'; 485 if (m_separator) { 486 if (F.isSeparator(m_separator)) dsep = m_separator; 487 else dsep = F.defaultSeparator; 488 } 489 static if (is(typeof(this.name) == string)) 490 string n = this.name; 491 else 492 string n = this.name.array; 493 return GenericPath!F.Segment2(n, dsep); 494 } 495 496 /// Compares two path segment names 497 bool opEquals(Segment2 other) 498 const nothrow @nogc { 499 try return equal(this.name, other.name) && this.hasSeparator == other.hasSeparator; 500 catch (Exception e) assert(false, e.msg); 501 } 502 /// ditto 503 bool opEquals(string name) 504 const nothrow @nogc { 505 import std.utf : byCodeUnit; 506 try return equal(this.name, name.byCodeUnit); 507 catch (Exception e) assert(false, e.msg); 508 } 509 } 510 511 private { 512 string m_path; 513 } 514 515 /// The default path segment separator character. 516 enum char defaultSeparator = Format.defaultSeparator; 517 518 /** Constructs a path from its string representation. 519 520 Throws: 521 A `PathValidationException` is thrown if the given path string 522 is not valid. 523 */ 524 this(string p) 525 { 526 auto err = Format.validatePath(p); 527 enforce!PathValidationException(err is null, err); 528 m_path = p; 529 } 530 531 /** Constructs a path from a single path segment. 532 533 This is equivalent to calling the range based constructor with a 534 single-element range. 535 */ 536 this(Segment segment) 537 { 538 import std.range : only; 539 this(only(segment)); 540 } 541 /// ditto 542 this(Segment2 segment) 543 { 544 import std.range : only; 545 this(only(segment)); 546 } 547 548 /** Constructs a path from an input range of `Segment`s. 549 550 Throws: 551 Since path segments are pre-validated, this constructor does not 552 throw an exception. 553 */ 554 this(R)(R segments) 555 if (isInputRange!R && is(ElementType!R : Segment)) 556 { 557 import std.array : appender; 558 auto dst = appender!string; 559 Format.toString(segments, dst); 560 m_path = dst.data; 561 } 562 /// ditto 563 this(R)(R segments) 564 if (isInputRange!R && is(ElementType!R : Segment2)) 565 { 566 import std.array : appender; 567 auto dst = appender!string; 568 Format.toString(segments, dst); 569 m_path = dst.data; 570 } 571 572 /** Constructs a path from its string representation. 573 574 This is equivalent to calling the string based constructor. 575 */ 576 static GenericPath fromString(string p) 577 { 578 return GenericPath(p); 579 } 580 581 /** Constructs a path from its string representation, skipping the 582 validation. 583 584 Note that it is required to pass a pre-validated path string 585 to this function. Debug builds will enforce this with an assertion. 586 */ 587 static GenericPath fromTrustedString(string p) 588 nothrow @nogc { 589 assert(Format.validatePath(p) is null, "Invalid trusted path."); 590 GenericPath ret; 591 ret.m_path = p; 592 return ret; 593 } 594 595 /// Tests if a certain character is a path segment separator. 596 static bool isSeparator(dchar ch) { return ch < 0x80 && Format.isSeparator(cast(char)ch); } 597 598 /// Tests if the path is represented by an empty string. 599 @property bool empty() const nothrow @nogc { return m_path.length == 0; } 600 601 /// Tests if the path is absolute. 602 @property bool absolute() const nothrow @nogc { return Format.getAbsolutePrefix(m_path).length > 0; } 603 604 /// Determines whether the path ends with a path separator (i.e. represents a folder specifically). 605 @property bool endsWithSlash() const nothrow @nogc { return m_path.length > 0 && Format.isSeparator(m_path[$-1]); } 606 /// ditto 607 @property void endsWithSlash(bool v) 608 nothrow { 609 bool ews = this.endsWithSlash; 610 if (!ews && v) m_path ~= Format.defaultSeparator; 611 else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME?: "/test//" -> "/test/" 612 } 613 614 /// Iterates over the path by `Segment`. 615 @property PathRange bySegment() const { return PathRange(m_path); } 616 617 618 /** Iterates over the individual segments of the path. 619 620 Returns a forward range of `Segment2`s. 621 */ 622 @property auto bySegment2() 623 const { 624 static struct R { 625 import std.traits : ReturnType; 626 627 private { 628 string m_path; 629 Segment2 m_front; 630 } 631 632 private this(string path) 633 { 634 m_path = path; 635 if (m_path.length) { 636 auto ap = Format.getAbsolutePrefix(m_path); 637 if (ap.length && !Format.isSeparator(ap[0])) 638 m_front = Segment2.fromTrustedEncodedString(null, '/'); 639 else readFront(); 640 } 641 } 642 643 @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment2.init; } 644 645 @property R save() { return this; } 646 647 @property Segment2 front() { return m_front; } 648 649 void popFront() 650 nothrow { 651 assert(m_front != Segment2.init); 652 if (m_path.length) readFront(); 653 else m_front = Segment2.init; 654 } 655 656 private void readFront() 657 { 658 auto n = Format.getFrontNode(m_path); 659 m_path = m_path[n.length .. $]; 660 661 char sep = '\0'; 662 if (Format.isSeparator(n[$-1])) { 663 sep = n[$-1]; 664 n = n[0 .. $-1]; 665 } 666 m_front = Segment2.fromTrustedEncodedString(n, sep); 667 assert(m_front != Segment2.init); 668 } 669 } 670 671 return R(m_path); 672 } 673 674 /// 675 unittest { 676 InetPath p = "foo/bar/baz"; 677 assert(p.bySegment2.equal([ 678 InetPath.Segment2("foo", '/'), 679 InetPath.Segment2("bar", '/'), 680 InetPath.Segment2("baz") 681 ])); 682 } 683 684 685 /** Iterates over the path by segment, each time returning the sub path 686 leading to that segment. 687 */ 688 @property auto byPrefix() 689 const nothrow @nogc { 690 static struct R { 691 import std.traits : ReturnType; 692 693 private { 694 string m_path; 695 string m_remainder; 696 } 697 698 private this(string path) 699 { 700 m_path = path; 701 m_remainder = path; 702 if (m_path.length) { 703 auto ap = Format.getAbsolutePrefix(m_path); 704 if (ap.length && !Format.isSeparator(ap[0])) 705 m_remainder = m_remainder[ap.length .. $]; 706 else popFront(); 707 } 708 } 709 710 @property bool empty() const nothrow @nogc 711 { 712 return m_path.length == 0; 713 } 714 715 @property R save() { return this; } 716 717 @property GenericPath front() 718 { 719 return GenericPath.fromTrustedString(m_path[0 .. $-m_remainder.length]); 720 } 721 722 void popFront() 723 nothrow { 724 assert(m_remainder.length > 0 || m_path.length > 0); 725 if (m_remainder.length) readFront(); 726 else m_path = ""; 727 } 728 729 private void readFront() 730 { 731 auto n = Format.getFrontNode(m_remainder); 732 m_remainder = m_remainder[n.length .. $]; 733 } 734 } 735 736 return R(m_path); 737 } 738 739 /// 740 unittest { 741 assert(InetPath("foo/bar/baz").byPrefix 742 .equal([ 743 InetPath("foo/"), 744 InetPath("foo/bar/"), 745 InetPath("foo/bar/baz") 746 ])); 747 748 assert(InetPath("/foo/bar").byPrefix 749 .equal([ 750 InetPath("/"), 751 InetPath("/foo/"), 752 InetPath("/foo/bar"), 753 ])); 754 } 755 756 757 /// Returns the trailing segment of the path. 758 @property Segment head() 759 const { 760 import std.array : array; 761 762 auto n = Format.getBackNode(m_path); 763 char sep = '\0'; 764 if (n.length > 0 && Format.isSeparator(n[$-1])) { 765 sep = n[$-1]; 766 n = n[0 .. $-1]; 767 } 768 769 static if (is(typeof(Format.decodeSingleSegment(n)) == string)) 770 string ndec = Format.decodeSingleSegment(n); 771 else 772 string ndec = Format.decodeSingleSegment(n).array; 773 774 return Segment.fromTrustedString(ndec, sep); 775 } 776 777 /// Returns the trailing segment of the path. 778 @property Segment2 head2() 779 const @nogc { 780 auto n = Format.getBackNode(m_path); 781 char sep = '\0'; 782 if (n.length > 0 && Format.isSeparator(n[$-1])) { 783 sep = n[$-1]; 784 n = n[0 .. $-1]; 785 } 786 return Segment2.fromTrustedEncodedString(n, sep); 787 } 788 789 /** Determines if the `parentPath` property is valid. 790 */ 791 bool hasParentPath() 792 const @nogc { 793 auto b = Format.getBackNode(m_path); 794 return b.length < m_path.length; 795 } 796 797 /** Returns a prefix of this path, where the last segment has been dropped. 798 799 Throws: 800 An `Exception` is thrown if this path has no parent path. Use 801 `hasParentPath` to test this upfront. 802 */ 803 GenericPath parentPath() 804 const @nogc { 805 auto b = Format.getBackNode(m_path); 806 static immutable Exception e = new Exception("Path has no parent path"); 807 if (b.length >= m_path.length) throw e; 808 return GenericPath.fromTrustedString(m_path[0 .. $ - b.length]); 809 } 810 811 /** Removes any redundant path segments and replaces all separators by the 812 default one. 813 814 The resulting path representation is suitable for basic semantic 815 comparison to other normalized paths. 816 817 Note that there are still ways for different normalized paths to 818 represent the same file. Examples of this are the tilde shortcut to the 819 home directory on Unix and Linux operating systems, symbolic or hard 820 links, and possibly environment variables are examples of this. 821 822 Throws: 823 Throws an `Exception` if an absolute path contains parent directory 824 segments ("..") that lead to a path that is a parent path of the 825 root path. 826 */ 827 void normalize() 828 { 829 import std.array : appender, join; 830 831 Segment2[] newnodes; 832 bool got_non_sep = false; 833 foreach (n; this.bySegment2) { 834 if (n.hasSeparator) n.separator = Format.defaultSeparator; 835 if (!got_non_sep) { 836 if (n.encodedName == "") newnodes ~= n; 837 else got_non_sep = true; 838 } 839 switch (n.encodedName) { 840 default: newnodes ~= n; break; 841 case "", ".": break; 842 case "..": 843 enforce(!this.absolute || newnodes.length > 0, "Path goes below root node."); 844 if (newnodes.length > 0 && newnodes[$-1].encodedName != "..") newnodes = newnodes[0 .. $-1]; 845 else newnodes ~= n; 846 break; 847 } 848 } 849 850 auto dst = appender!string; 851 Format.toString(newnodes, dst); 852 m_path = dst.data; 853 } 854 855 /// 856 unittest { 857 auto path = WindowsPath("C:\\test/foo/./bar///../baz"); 858 path.normalize(); 859 assert(path.toString() == "C:\\test\\foo\\baz", path.toString()); 860 861 path = WindowsPath("foo/../../bar/"); 862 path.normalize(); 863 assert(path.toString() == "..\\bar\\"); 864 } 865 866 /// Returns the string representation of the path. 867 string toString() const nothrow @nogc { return m_path; } 868 869 /// Computes a hash sum, enabling storage within associative arrays. 870 size_t toHash() const nothrow @trusted 871 { 872 try return typeid(string).getHash(&m_path); 873 catch (Exception e) assert(false, "getHash for string throws!?"); 874 } 875 876 /** Compares two path objects. 877 878 Note that the exact string representation of the two paths will be 879 compared. To get a basic semantic comparison, the paths must be 880 normalized first. 881 */ 882 bool opEquals(GenericPath other) const @nogc { return this.m_path == other.m_path; } 883 884 /** Converts the path to a different path format. 885 886 Throws: 887 A `PathValidationException` will be thrown if the path is not 888 representable in the requested path format. This can happen 889 especially when converting Posix or Internet paths to windows paths, 890 since Windows paths cannot contain a number of characters that the 891 other representations can, in theory. 892 */ 893 P opCast(P)() const if (isInstanceOf!(.GenericPath, P)) { 894 static if (is(P == GenericPath)) return this; 895 else return P(this.bySegment2.map!(n => cast(P.Segment2)n)); 896 } 897 898 /** Concatenates two paths. 899 900 The right hand side must represent a relative path. 901 */ 902 GenericPath opBinary(string op : "~")(string subpath) const { return this ~ GenericPath(subpath); } 903 /// ditto 904 GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); } 905 /// ditto 906 GenericPath opBinary(string op : "~")(Segment2 subpath) const { return this ~ GenericPath(subpath); } 907 /// ditto 908 GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment subpath) const { return this ~ cast(Segment)(subpath); } 909 /// ditto 910 GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment2 subpath) const { return this ~ cast(Segment2)(subpath); } 911 /// ditto 912 GenericPath opBinary(string op : "~")(GenericPath subpath) const nothrow { 913 assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path."); 914 if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path); 915 else return GenericPath.fromTrustedString(m_path ~ Format.defaultSeparator ~ subpath.m_path); 916 } 917 /// ditto 918 GenericPath opBinary(string op : "~", F)(GenericPath!F subpath) const if (!is(F == Format)) { return this ~ cast(GenericPath)subpath; } 919 /// ditto 920 GenericPath opBinary(string op : "~", R)(R entries) const nothrow 921 if (isInputRange!R && is(ElementType!R : Segment)) 922 { 923 return this ~ GenericPath(entries); 924 } 925 /// ditto 926 GenericPath opBinary(string op : "~", R)(R entries) const nothrow 927 if (isInputRange!R && is(ElementType!R : Segment2)) 928 { 929 return this ~ GenericPath(entries); 930 } 931 932 /// Appends a relative path to this path. 933 void opOpAssign(string op : "~", T)(T op) { this = this ~ op; } 934 935 /** Tests whether the given path is a prefix of this path. 936 937 Any path separators will be ignored during the comparison. 938 */ 939 bool startsWith(GenericPath prefix) 940 const nothrow { 941 return bySegment2.map!(n => n.name).startsWith(prefix.bySegment2.map!(n => n.name)); 942 } 943 } 944 945 unittest { 946 assert(PosixPath("hello/world").bySegment.equal([PosixPath.Segment("hello",'/'), PosixPath.Segment("world")])); 947 assert(PosixPath("/hello/world/").bySegment.equal([PosixPath.Segment("",'/'), PosixPath.Segment("hello",'/'), PosixPath.Segment("world",'/')])); 948 assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")])); 949 assert(WindowsPath("hello/world").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world")])); 950 assert(WindowsPath("/hello/world/").bySegment.equal([WindowsPath.Segment("",'/'), WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world",'/')])); 951 assert(WindowsPath("hello\\w/orld").bySegment.equal([WindowsPath.Segment("hello",'\\'), WindowsPath.Segment("w",'/'), WindowsPath.Segment("orld")])); 952 assert(WindowsPath("hello/w\\orld").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("w",'\\'), WindowsPath.Segment("orld")])); 953 } 954 955 unittest { 956 assert(PosixPath("hello/world").bySegment2.equal([PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world")])); 957 assert(PosixPath("/hello/world/").bySegment2.equal([PosixPath.Segment2("",'/'), PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world",'/')])); 958 assert(PosixPath("hello\\world").bySegment2.equal([PosixPath.Segment2("hello\\world")])); 959 assert(WindowsPath("hello/world").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world")])); 960 assert(WindowsPath("/hello/world/").bySegment2.equal([WindowsPath.Segment2("",'/'), WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world",'/')])); 961 assert(WindowsPath("hello\\w/orld").bySegment2.equal([WindowsPath.Segment2("hello",'\\'), WindowsPath.Segment2("w",'/'), WindowsPath.Segment2("orld")])); 962 assert(WindowsPath("hello/w\\orld").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("w",'\\'), WindowsPath.Segment2("orld")])); 963 964 assert(PosixPath("hello/world").byPrefix.equal([PosixPath("hello/"), PosixPath("hello/world")])); 965 assert(PosixPath("/hello/world/").byPrefix.equal([PosixPath("/"), PosixPath("/hello/"), PosixPath("/hello/world/")])); 966 assert(WindowsPath("C:\\Windows").byPrefix.equal([WindowsPath("C:\\"), WindowsPath("C:\\Windows")])); 967 } 968 969 unittest 970 { 971 { 972 auto unc = "\\\\server\\share\\path"; 973 auto uncp = WindowsPath(unc); 974 assert(uncp.absolute); 975 uncp.normalize(); 976 version(Windows) assert(uncp.toNativeString() == unc); 977 assert(uncp.absolute); 978 assert(!uncp.endsWithSlash); 979 } 980 981 { 982 auto abspath = "/test/path/"; 983 auto abspathp = PosixPath(abspath); 984 assert(abspathp.toString() == abspath); 985 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 986 assert(abspathp.absolute); 987 assert(abspathp.endsWithSlash); 988 alias S = PosixPath.Segment; 989 assert(abspathp.bySegment.equal([S("", '/'), S("test", '/'), S("path", '/')])); 990 } 991 992 { 993 auto relpath = "test/path/"; 994 auto relpathp = PosixPath(relpath); 995 assert(relpathp.toString() == relpath); 996 version(Windows) assert(relpathp.toNativeString() == "test/path/"); 997 else assert(relpathp.toNativeString() == relpath); 998 assert(!relpathp.absolute); 999 assert(relpathp.endsWithSlash); 1000 alias S = PosixPath.Segment; 1001 assert(relpathp.bySegment.equal([S("test", '/'), S("path", '/')])); 1002 } 1003 1004 { 1005 auto winpath = "C:\\windows\\test"; 1006 auto winpathp = WindowsPath(winpath); 1007 assert(winpathp.toString() == "C:\\windows\\test"); 1008 assert((cast(PosixPath)winpathp).toString() == "/C:/windows/test", (cast(PosixPath)winpathp).toString()); 1009 version(Windows) assert(winpathp.toNativeString() == winpath); 1010 else assert(winpathp.toNativeString() == "/C:/windows/test"); 1011 assert(winpathp.absolute); 1012 assert(!winpathp.endsWithSlash); 1013 alias S = WindowsPath.Segment; 1014 assert(winpathp.bySegment.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")])); 1015 } 1016 1017 { 1018 auto dotpath = "/test/../test2/././x/y"; 1019 auto dotpathp = PosixPath(dotpath); 1020 assert(dotpathp.toString() == "/test/../test2/././x/y"); 1021 dotpathp.normalize(); 1022 assert(dotpathp.toString() == "/test2/x/y", dotpathp.toString()); 1023 } 1024 1025 { 1026 auto dotpath = "/test/..////test2//./x/y"; 1027 auto dotpathp = PosixPath(dotpath); 1028 assert(dotpathp.toString() == "/test/..////test2//./x/y"); 1029 dotpathp.normalize(); 1030 assert(dotpathp.toString() == "/test2/x/y"); 1031 } 1032 1033 assert(WindowsPath("C:\\Windows").absolute); 1034 assert((cast(InetPath)WindowsPath("C:\\Windows")).toString() == "/C:/Windows"); 1035 assert((WindowsPath("C:\\Windows") ~ InetPath("test/this")).toString() == "C:\\Windows\\test/this"); 1036 assert(InetPath("/C:/Windows").absolute); 1037 assert((cast(WindowsPath)InetPath("/C:/Windows")).toString() == "C:/Windows"); 1038 assert((InetPath("/C:/Windows") ~ WindowsPath("test\\this")).toString() == "/C:/Windows/test/this"); 1039 assert((InetPath("") ~ WindowsPath("foo\\bar")).toString() == "foo/bar"); 1040 assert((cast(InetPath)WindowsPath("C:\\Windows\\")).toString() == "/C:/Windows/"); 1041 1042 assert(NativePath("").empty); 1043 1044 assert(PosixPath("/") ~ NativePath("foo/bar") == PosixPath("/foo/bar")); 1045 assert(PosixPath("") ~ NativePath("foo/bar") == PosixPath("foo/bar")); 1046 assert(PosixPath("foo") ~ NativePath("bar") == PosixPath("foo/bar")); 1047 assert(PosixPath("foo/") ~ NativePath("bar") == PosixPath("foo/bar")); 1048 } 1049 1050 unittest 1051 { 1052 { 1053 auto unc = "\\\\server\\share\\path"; 1054 auto uncp = WindowsPath(unc); 1055 assert(uncp.absolute); 1056 uncp.normalize(); 1057 version(Windows) assert(uncp.toNativeString() == unc); 1058 assert(uncp.absolute); 1059 assert(!uncp.endsWithSlash); 1060 } 1061 1062 { 1063 auto abspath = "/test/path/"; 1064 auto abspathp = PosixPath(abspath); 1065 assert(abspathp.toString() == abspath); 1066 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 1067 assert(abspathp.absolute); 1068 assert(abspathp.endsWithSlash); 1069 alias S = PosixPath.Segment2; 1070 assert(abspathp.bySegment2.equal([S("", '/'), S("test", '/'), S("path", '/')])); 1071 } 1072 1073 { 1074 auto relpath = "test/path/"; 1075 auto relpathp = PosixPath(relpath); 1076 assert(relpathp.toString() == relpath); 1077 version(Windows) assert(relpathp.toNativeString() == "test/path/"); 1078 else assert(relpathp.toNativeString() == relpath); 1079 assert(!relpathp.absolute); 1080 assert(relpathp.endsWithSlash); 1081 alias S = PosixPath.Segment2; 1082 assert(relpathp.bySegment2.equal([S("test", '/'), S("path", '/')])); 1083 } 1084 1085 { 1086 auto winpath = "C:\\windows\\test"; 1087 auto winpathp = WindowsPath(winpath); 1088 assert(winpathp.toString() == "C:\\windows\\test"); 1089 assert((cast(PosixPath)winpathp).toString() == "/C:/windows/test", (cast(PosixPath)winpathp).toString()); 1090 version(Windows) assert(winpathp.toNativeString() == winpath); 1091 else assert(winpathp.toNativeString() == "/C:/windows/test"); 1092 assert(winpathp.absolute); 1093 assert(!winpathp.endsWithSlash); 1094 alias S = WindowsPath.Segment2; 1095 assert(winpathp.bySegment2.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")])); 1096 } 1097 } 1098 1099 @safe unittest { 1100 import std.array : appender; 1101 auto app = appender!(PosixPath[]); 1102 void test1(PosixPath p) { app.put(p); } 1103 void test2(PosixPath[] ps) { app.put(ps); } 1104 //void test3(const(PosixPath) p) { app.put(p); } // DMD issue 17251 1105 //void test4(const(PosixPath)[] ps) { app.put(ps); } 1106 } 1107 1108 unittest { 1109 import std.exception : assertThrown, assertNotThrown; 1110 1111 assertThrown!PathValidationException(WindowsPath.Segment("foo/bar")); 1112 assertThrown!PathValidationException(PosixPath.Segment("foo/bar")); 1113 assertNotThrown!PathValidationException(InetPath.Segment("foo/bar")); 1114 1115 auto p = InetPath("/foo%2fbar/"); 1116 assert(p.bySegment.equal([InetPath.Segment("",'/'), InetPath.Segment("foo/bar",'/')])); 1117 p ~= InetPath.Segment("baz/bam"); 1118 assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); 1119 } 1120 1121 unittest { 1122 import std.exception : assertThrown, assertNotThrown; 1123 1124 assertThrown!PathValidationException(WindowsPath.Segment2("foo/bar")); 1125 assertThrown!PathValidationException(PosixPath.Segment2("foo/bar")); 1126 assertNotThrown!PathValidationException(InetPath.Segment2("foo/bar")); 1127 1128 auto p = InetPath("/foo%2fbar/"); 1129 import std.conv : to; 1130 assert(p.bySegment2.equal([InetPath.Segment2("",'/'), InetPath.Segment2("foo/bar",'/')]), p.bySegment2.to!string); 1131 p ~= InetPath.Segment2("baz/bam"); 1132 assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); 1133 } 1134 1135 unittest { 1136 assert(!PosixPath("").hasParentPath); 1137 assert(!PosixPath("/").hasParentPath); 1138 assert(!PosixPath("foo\\bar").hasParentPath); 1139 assert(PosixPath("foo/bar").parentPath.toString() == "foo/"); 1140 assert(PosixPath("./foo").parentPath.toString() == "./"); 1141 assert(PosixPath("./foo").parentPath.toString() == "./"); 1142 1143 assert(!WindowsPath("").hasParentPath); 1144 assert(!WindowsPath("/").hasParentPath); 1145 assert(WindowsPath("foo\\bar").parentPath.toString() == "foo\\"); 1146 assert(WindowsPath("foo/bar").parentPath.toString() == "foo/"); 1147 assert(WindowsPath("./foo").parentPath.toString() == "./"); 1148 assert(WindowsPath("./foo").parentPath.toString() == "./"); 1149 1150 assert(!InetPath("").hasParentPath); 1151 assert(!InetPath("/").hasParentPath); 1152 assert(InetPath("foo/bar").parentPath.toString() == "foo/"); 1153 assert(InetPath("foo/bar%2Fbaz").parentPath.toString() == "foo/"); 1154 assert(InetPath("./foo").parentPath.toString() == "./"); 1155 assert(InetPath("./foo").parentPath.toString() == "./"); 1156 } 1157 1158 unittest { 1159 assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar"); 1160 } 1161 1162 unittest { 1163 assert(WindowsPath([WindowsPath.Segment2("foo"), WindowsPath.Segment2("bar")]).toString() == "foo\\bar"); 1164 } 1165 1166 /// Thrown when an invalid string representation of a path is detected. 1167 class PathValidationException : Exception { 1168 this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null) 1169 pure nothrow @nogc @safe 1170 { 1171 super(text, file, line, next); 1172 } 1173 } 1174 1175 /** Implements Windows path semantics. 1176 1177 See_also: `WindowsPath` 1178 */ 1179 struct WindowsPathFormat { 1180 static void toString(I, O)(I segments, O dst) 1181 if (isInputRange!I && isOutputRange!(O, char)) 1182 { 1183 char sep(char s) { return isSeparator(s) ? s : defaultSeparator; } 1184 1185 if (segments.empty) return; 1186 1187 if (segments.front.name == "" && segments.front.separator) { 1188 auto s = segments.front.separator; 1189 segments.popFront(); 1190 if (segments.empty || !segments.front.name.endsWith(":")) 1191 dst.put(sep(s)); 1192 } 1193 1194 char lastsep = '\0'; 1195 bool first = true; 1196 foreach (s; segments) { 1197 if (!first || lastsep) dst.put(sep(lastsep)); 1198 else first = false; 1199 dst.put(s.name); 1200 lastsep = s.separator; 1201 } 1202 if (lastsep) dst.put(sep(lastsep)); 1203 } 1204 1205 unittest { 1206 import std.array : appender; 1207 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1208 string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } 1209 1210 assert(str() == ""); 1211 assert(str(Segment("",'/')) == "/"); 1212 assert(str(Segment("",'/'), Segment("foo")) == "/foo"); 1213 assert(str(Segment("",'\\')) == "\\"); 1214 assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); 1215 assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); 1216 assert(str(Segment("",'\\'), Segment("foo",'\\')) == "\\foo\\"); 1217 assert(str(Segment("f oo")) == "f oo"); 1218 assert(str(Segment("",'\\'), Segment("C:")) == "C:"); 1219 assert(str(Segment("",'\\'), Segment("C:", '/')) == "C:/"); 1220 assert(str(Segment("foo",'\\'), Segment("C:")) == "foo\\C:"); 1221 assert(str(Segment("foo"), Segment("bar")) == "foo\\bar"); 1222 } 1223 1224 @safe nothrow pure: 1225 enum defaultSeparator = '\\'; 1226 1227 static bool isSeparator(dchar ch) 1228 @nogc { 1229 import std.algorithm.comparison : among; 1230 return ch.among!('\\', '/') != 0; 1231 } 1232 1233 static string getAbsolutePrefix(string path) 1234 @nogc { 1235 if (!path.length) return null; 1236 1237 if (isSeparator(path[0])) { 1238 return path[0 .. 1]; 1239 } 1240 1241 foreach (i; 1 .. path.length) 1242 if (isSeparator(path[i])) { 1243 if (path[i-1] == ':') return path[0 .. i+1]; 1244 break; 1245 } 1246 1247 return path[$-1] == ':' ? path : null; 1248 } 1249 1250 unittest { 1251 assert(getAbsolutePrefix("test") == ""); 1252 assert(getAbsolutePrefix("test/") == ""); 1253 assert(getAbsolutePrefix("/test") == "/"); 1254 assert(getAbsolutePrefix("\\test") == "\\"); 1255 assert(getAbsolutePrefix("C:\\") == "C:\\"); 1256 assert(getAbsolutePrefix("C:") == "C:"); 1257 assert(getAbsolutePrefix("C:\\test") == "C:\\"); 1258 assert(getAbsolutePrefix("C:\\test\\") == "C:\\"); 1259 assert(getAbsolutePrefix("C:/") == "C:/"); 1260 assert(getAbsolutePrefix("C:/test") == "C:/"); 1261 assert(getAbsolutePrefix("C:/test/") == "C:/"); 1262 assert(getAbsolutePrefix("\\\\server") == "\\"); 1263 assert(getAbsolutePrefix("\\\\server\\") == "\\"); 1264 assert(getAbsolutePrefix("\\\\.\\") == "\\"); 1265 assert(getAbsolutePrefix("\\\\?\\") == "\\"); 1266 } 1267 1268 static string getFrontNode(string path) 1269 @nogc { 1270 foreach (i; 0 .. path.length) 1271 if (isSeparator(path[i])) 1272 return path[0 .. i+1]; 1273 return path; 1274 } 1275 1276 unittest { 1277 assert(getFrontNode("") == ""); 1278 assert(getFrontNode("/bar") == "/"); 1279 assert(getFrontNode("foo/bar") == "foo/"); 1280 assert(getFrontNode("foo/") == "foo/"); 1281 assert(getFrontNode("foo") == "foo"); 1282 assert(getFrontNode("\\bar") == "\\"); 1283 assert(getFrontNode("foo\\bar") == "foo\\"); 1284 assert(getFrontNode("foo\\") == "foo\\"); 1285 } 1286 1287 static string getBackNode(string path) 1288 @nogc { 1289 if (!path.length) return path; 1290 foreach_reverse (i; 0 .. path.length-1) 1291 if (isSeparator(path[i])) 1292 return path[i+1 .. $]; 1293 return path; 1294 } 1295 1296 unittest { 1297 assert(getBackNode("") == ""); 1298 assert(getBackNode("/bar") == "bar"); 1299 assert(getBackNode("foo/bar") == "bar"); 1300 assert(getBackNode("foo/") == "foo/"); 1301 assert(getBackNode("foo") == "foo"); 1302 assert(getBackNode("\\bar") == "bar"); 1303 assert(getBackNode("foo\\bar") == "bar"); 1304 assert(getBackNode("foo\\") == "foo\\"); 1305 } 1306 1307 deprecated("Use decodeSingleSegment instead.") 1308 static auto decodeSegment(S)(string segment) 1309 { 1310 static struct R { 1311 S[2] items; 1312 size_t i = items.length; 1313 this(S s) { i = 1; items[i] = s; } 1314 this(S a, S b) { i = 0; items[0] = a; items[1] = b; } 1315 @property ref S front() { return items[i]; } 1316 @property bool empty() const { return i >= items.length; } 1317 void popFront() { i++; } 1318 } 1319 1320 assert(segment.length > 0, "Path segment string must not be empty."); 1321 1322 char sep = '\0'; 1323 if (!segment.length) return R(S.fromTrustedString(null)); 1324 if (isSeparator(segment[$-1])) { 1325 sep = segment[$-1]; 1326 segment = segment[0 .. $-1]; 1327 } 1328 1329 // output an absolute marker segment for "C:\" style absolute segments 1330 if (segment.length > 0 && segment[$-1] == ':') 1331 return R(S.fromTrustedString("", '/'), S.fromTrustedString(segment, sep)); 1332 1333 return R(S.fromTrustedString(segment, sep)); 1334 } 1335 1336 deprecated unittest { 1337 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1338 assert(decodeSegment!Segment("foo").equal([Segment("foo")])); 1339 assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); 1340 assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo%20o", '\\')])); 1341 assert(decodeSegment!Segment("C:\\").equal([Segment("",'/'), Segment("C:", '\\')])); 1342 assert(decodeSegment!Segment("bar:\\").equal([Segment("",'/'), Segment("bar:", '\\')])); 1343 } 1344 1345 static string decodeSingleSegment(string segment) 1346 @nogc { 1347 assert(segment.length == 0 || segment[$-1] != '/'); 1348 return segment; 1349 } 1350 1351 unittest { 1352 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1353 assert(decodeSingleSegment("foo") == "foo"); 1354 assert(decodeSingleSegment("fo%20o") == "fo%20o"); 1355 assert(decodeSingleSegment("C:") == "C:"); 1356 assert(decodeSingleSegment("bar:") == "bar:"); 1357 } 1358 1359 static string validatePath(string path) 1360 @nogc { 1361 import std.algorithm.comparison : among; 1362 1363 // skip UNC prefix 1364 if (path.startsWith("\\\\")) { 1365 path = path[2 .. $]; 1366 while (path.length && !isSeparator(path[0])) { 1367 if (path[0] < 32 || path[0].among('<', '>', '|')) 1368 return "Invalid character in UNC host name."; 1369 path = path[1 .. $]; 1370 } 1371 if (path.length) path = path[1 .. $]; 1372 } 1373 1374 // stricter validation for the rest 1375 bool had_sep = false; 1376 foreach (i, char c; path) { 1377 if (c < 32 || c.among!('<', '>', '|', '?')) 1378 return "Invalid character in path."; 1379 if (isSeparator(c)) had_sep = true; 1380 else if (c == ':' && (had_sep || i+1 < path.length && !isSeparator(path[i+1]))) 1381 return "Colon in path that is not part of a drive name."; 1382 1383 } 1384 return null; 1385 } 1386 1387 static string validateDecodedSegment(string segment) 1388 @nogc { 1389 auto pe = validatePath(segment); 1390 if (pe) return pe; 1391 foreach (char c; segment) 1392 if (isSeparator(c)) 1393 return "Path segment contains separator character."; 1394 return null; 1395 } 1396 1397 unittest { 1398 assert(validatePath("c:\\foo") is null); 1399 assert(validatePath("\\\\?\\c:\\foo") is null); 1400 assert(validatePath("//?\\c:\\foo") !is null); 1401 assert(validatePath("-foo/bar\\*\\baz") is null); 1402 assert(validatePath("foo\0bar") !is null); 1403 assert(validatePath("foo\tbar") !is null); 1404 assert(validatePath("\\c:\\foo") !is null); 1405 assert(validatePath("c:d\\foo") !is null); 1406 assert(validatePath("foo\\b:ar") !is null); 1407 assert(validatePath("foo\\bar:\\baz") !is null); 1408 } 1409 1410 static string encodeSegment(string segment) 1411 { 1412 assert(segment.length == 0 || segment[$-1] != '/'); 1413 return segment; 1414 } 1415 } 1416 1417 1418 /** Implements Unix/Linux path semantics. 1419 1420 See_also: `WindowsPath` 1421 */ 1422 struct PosixPathFormat { 1423 static void toString(I, O)(I segments, O dst) 1424 { 1425 char lastsep = '\0'; 1426 bool first = true; 1427 foreach (s; segments) { 1428 if (!first || lastsep) dst.put('/'); 1429 else first = false; 1430 dst.put(s.name); 1431 lastsep = s.separator; 1432 } 1433 if (lastsep) dst.put('/'); 1434 } 1435 1436 unittest { 1437 import std.array : appender; 1438 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1439 string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } 1440 1441 assert(str() == ""); 1442 assert(str(Segment("",'/')) == "/"); 1443 assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); 1444 assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); 1445 assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); 1446 assert(str(Segment("f oo")) == "f oo"); 1447 assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); 1448 } 1449 1450 @safe nothrow pure: 1451 enum defaultSeparator = '/'; 1452 1453 static bool isSeparator(dchar ch) 1454 @nogc { 1455 return ch == '/'; 1456 } 1457 1458 static string getAbsolutePrefix(string path) 1459 @nogc { 1460 if (path.length > 0 && path[0] == '/') 1461 return path[0 .. 1]; 1462 return null; 1463 } 1464 1465 unittest { 1466 assert(getAbsolutePrefix("/") == "/"); 1467 assert(getAbsolutePrefix("/test") == "/"); 1468 assert(getAbsolutePrefix("/test/") == "/"); 1469 assert(getAbsolutePrefix("test/") == ""); 1470 assert(getAbsolutePrefix("") == ""); 1471 assert(getAbsolutePrefix("./") == ""); 1472 } 1473 1474 static string getFrontNode(string path) 1475 @nogc { 1476 import std..string : indexOf; 1477 auto idx = path.indexOf('/'); 1478 return idx < 0 ? path : path[0 .. idx+1]; 1479 } 1480 1481 unittest { 1482 assert(getFrontNode("") == ""); 1483 assert(getFrontNode("/bar") == "/"); 1484 assert(getFrontNode("foo/bar") == "foo/"); 1485 assert(getFrontNode("foo/") == "foo/"); 1486 assert(getFrontNode("foo") == "foo"); 1487 } 1488 1489 static string getBackNode(string path) 1490 @nogc { 1491 if (!path.length) return path; 1492 foreach_reverse (i; 0 .. path.length-1) 1493 if (path[i] == '/') 1494 return path[i+1 .. $]; 1495 return path; 1496 } 1497 1498 unittest { 1499 assert(getBackNode("") == ""); 1500 assert(getBackNode("/bar") == "bar"); 1501 assert(getBackNode("foo/bar") == "bar"); 1502 assert(getBackNode("foo/") == "foo/"); 1503 assert(getBackNode("foo") == "foo"); 1504 } 1505 1506 static string validatePath(string path) 1507 @nogc { 1508 foreach (char c; path) 1509 if (c == '\0') 1510 return "Invalid NUL character in file name"; 1511 return null; 1512 } 1513 1514 static string validateDecodedSegment(string segment) 1515 @nogc { 1516 auto pe = validatePath(segment); 1517 if (pe) return pe; 1518 foreach (char c; segment) 1519 if (isSeparator(c)) 1520 return "Path segment contains separator character."; 1521 return null; 1522 } 1523 1524 unittest { 1525 assert(validatePath("-foo/bar*/baz?") is null); 1526 assert(validatePath("foo\0bar") !is null); 1527 } 1528 1529 deprecated("Use decodeSingleSegment instead.") 1530 static auto decodeSegment(S)(string segment) 1531 { 1532 assert(segment.length > 0, "Path segment string must not be empty."); 1533 import std.range : only; 1534 if (!segment.length) return only(S.fromTrustedString(null, '/')); 1535 if (segment[$-1] == '/') 1536 return only(S.fromTrustedString(segment[0 .. $-1], '/')); 1537 return only(S.fromTrustedString(segment)); 1538 } 1539 1540 deprecated unittest { 1541 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1542 assert(decodeSegment!Segment("foo").equal([Segment("foo")])); 1543 assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); 1544 assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo%20o\\")])); 1545 } 1546 1547 static string decodeSingleSegment(string segment) 1548 @nogc { 1549 assert(segment.length == 0 || segment[$-1] != '/'); 1550 return segment; 1551 } 1552 1553 unittest { 1554 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1555 assert(decodeSingleSegment("foo") == "foo"); 1556 assert(decodeSingleSegment("fo%20o\\") == "fo%20o\\"); 1557 } 1558 1559 static string encodeSegment(string segment) 1560 { 1561 assert(segment.length == 0 || segment[$-1] != '/'); 1562 return segment; 1563 } 1564 } 1565 1566 1567 /** Implements URI/Internet path semantics. 1568 1569 See_also: `WindowsPath` 1570 */ 1571 struct InetPathFormat { 1572 static void toString(I, O)(I segments, O dst) 1573 { 1574 char lastsep = '\0'; 1575 bool first = true; 1576 foreach (e; segments) { 1577 if (!first || lastsep) dst.put('/'); 1578 else first = false; 1579 static if (is(typeof(e.encodedName))) 1580 dst.put(e.encodedName); 1581 else encodeSegment(dst, e.name); 1582 lastsep = e.separator; 1583 } 1584 if (lastsep) dst.put('/'); 1585 } 1586 1587 unittest { 1588 import std.array : appender; 1589 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1590 string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } 1591 assert(str() == ""); 1592 assert(str(Segment("",'/')) == "/"); 1593 assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); 1594 assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); 1595 assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); 1596 assert(str(Segment("f oo")) == "f%20oo"); 1597 assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); 1598 } 1599 1600 @safe pure nothrow: 1601 enum defaultSeparator = '/'; 1602 1603 static bool isSeparator(dchar ch) 1604 @nogc { 1605 return ch == '/'; 1606 } 1607 1608 static string getAbsolutePrefix(string path) 1609 @nogc { 1610 if (path.length > 0 && path[0] == '/') 1611 return path[0 .. 1]; 1612 return null; 1613 } 1614 1615 unittest { 1616 assert(getAbsolutePrefix("/") == "/"); 1617 assert(getAbsolutePrefix("/test") == "/"); 1618 assert(getAbsolutePrefix("/test/") == "/"); 1619 assert(getAbsolutePrefix("test/") == ""); 1620 assert(getAbsolutePrefix("") == ""); 1621 assert(getAbsolutePrefix("./") == ""); 1622 } 1623 1624 static string getFrontNode(string path) 1625 @nogc { 1626 import std..string : indexOf; 1627 auto idx = path.indexOf('/'); 1628 return idx < 0 ? path : path[0 .. idx+1]; 1629 } 1630 1631 unittest { 1632 assert(getFrontNode("") == ""); 1633 assert(getFrontNode("/bar") == "/"); 1634 assert(getFrontNode("foo/bar") == "foo/"); 1635 assert(getFrontNode("foo/") == "foo/"); 1636 assert(getFrontNode("foo") == "foo"); 1637 } 1638 1639 static string getBackNode(string path) 1640 @nogc { 1641 import std..string : lastIndexOf; 1642 1643 if (!path.length) return path; 1644 ptrdiff_t idx; 1645 try idx = path[0 .. $-1].lastIndexOf('/'); 1646 catch (Exception e) assert(false, e.msg); 1647 if (idx >= 0) return path[idx+1 .. $]; 1648 return path; 1649 } 1650 1651 unittest { 1652 assert(getBackNode("") == ""); 1653 assert(getBackNode("/bar") == "bar"); 1654 assert(getBackNode("foo/bar") == "bar"); 1655 assert(getBackNode("foo/") == "foo/"); 1656 assert(getBackNode("foo") == "foo"); 1657 } 1658 1659 static string validatePath(string path) 1660 @nogc { 1661 for (size_t i = 0; i < path.length; i++) { 1662 switch (path[i]) { 1663 default: 1664 return "Invalid character in internet path."; 1665 // unreserved 1666 case 'A': .. case 'Z': 1667 case 'a': .. case 'z': 1668 case '0': .. case '9': 1669 case '-', '.', '_', '~': 1670 // subdelims 1671 case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': 1672 // additional delims 1673 case ':', '@': 1674 // segment delimiter 1675 case '/': 1676 break; 1677 case '%': // pct encoding 1678 if (path.length < i+3) 1679 return "Unterminated percent encoding sequence in internet path."; 1680 foreach (j; 0 .. 2) { 1681 switch (path[++i]) { 1682 default: return "Invalid percent encoding sequence in internet path."; 1683 case '0': .. case '9': 1684 case 'a': .. case 'f': 1685 case 'A': .. case 'F': 1686 break; 1687 } 1688 } 1689 break; 1690 } 1691 } 1692 return null; 1693 } 1694 1695 static string validateDecodedSegment(string seg) 1696 @nogc { 1697 return null; 1698 } 1699 1700 unittest { 1701 assert(validatePath("") is null); 1702 assert(validatePath("/") is null); 1703 assert(validatePath("/test") is null); 1704 assert(validatePath("test") is null); 1705 assert(validatePath("/C:/test") is null); 1706 assert(validatePath("/test%ab") is null); 1707 assert(validatePath("/test%ag") !is null); 1708 assert(validatePath("/test%a") !is null); 1709 assert(validatePath("/test%") !is null); 1710 assert(validatePath("/test§") !is null); 1711 assert(validatePath("föö") !is null); 1712 } 1713 1714 deprecated("Use decodeSingleSegment instead.") 1715 static auto decodeSegment(S)(string segment) 1716 { 1717 import std.algorithm.searching : any; 1718 import std.array : array; 1719 import std.exception : assumeUnique; 1720 import std.range : only; 1721 import std.utf : byCodeUnit; 1722 1723 if (!segment.length) return only(S.fromTrustedString(null)); 1724 char sep = '\0'; 1725 if (segment[$-1] == '/') { 1726 sep = '/'; 1727 segment = segment[0 .. $-1]; 1728 } 1729 1730 if (!segment.byCodeUnit.any!(c => c == '%')) 1731 return only(S(segment, sep)); 1732 string n = decodeSingleSegment(segment).array; 1733 return only(S(n, sep)); 1734 } 1735 1736 deprecated unittest { 1737 struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} 1738 assert(decodeSegment!Segment("foo").equal([Segment("foo")])); 1739 assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); 1740 assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo o\\")])); 1741 assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")])); 1742 } 1743 1744 static auto decodeSingleSegment(string segment) 1745 @nogc { 1746 import std..string : indexOf; 1747 1748 static int hexDigit(char ch) @safe nothrow @nogc { 1749 assert(ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f'); 1750 if (ch >= '0' && ch <= '9') return ch - '0'; 1751 else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; 1752 else return ch - 'A' + 10; 1753 } 1754 1755 static struct R { 1756 @safe pure nothrow @nogc: 1757 1758 private { 1759 string m_str; 1760 size_t m_index; 1761 } 1762 1763 this(string s) 1764 { 1765 m_str = s; 1766 } 1767 1768 @property bool empty() const { return m_index >= m_str.length; } 1769 1770 @property char front() 1771 const { 1772 auto ch = m_str[m_index]; 1773 if (ch != '%') return ch; 1774 1775 auto a = m_str[m_index+1]; 1776 auto b = m_str[m_index+2]; 1777 return cast(char)(16 * hexDigit(a) + hexDigit(b)); 1778 } 1779 1780 @property void popFront() 1781 { 1782 assert(!empty); 1783 if (m_str[m_index] == '%') m_index += 3; 1784 else m_index++; 1785 } 1786 } 1787 1788 return R(segment); 1789 } 1790 1791 unittest { 1792 scope (failure) assert(false); 1793 1794 assert(decodeSingleSegment("foo").equal("foo")); 1795 assert(decodeSingleSegment("fo%20o\\").equal("fo o\\")); 1796 assert(decodeSingleSegment("foo%20").equal("foo ")); 1797 } 1798 1799 1800 static string encodeSegment(string segment) 1801 { 1802 import std.array : appender; 1803 1804 foreach (i, char c; segment) { 1805 switch (c) { 1806 default: 1807 auto ret = appender!string; 1808 ret.put(segment[0 .. i]); 1809 encodeSegment(ret, segment[i .. $]); 1810 return ret.data; 1811 case 'a': .. case 'z': 1812 case 'A': .. case 'Z': 1813 case '0': .. case '9': 1814 case '-', '.', '_', '~': 1815 case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': 1816 case ':', '@': 1817 break; 1818 } 1819 } 1820 1821 return segment; 1822 } 1823 1824 unittest { 1825 assert(encodeSegment("foo") == "foo"); 1826 assert(encodeSegment("foo bar") == "foo%20bar"); 1827 } 1828 1829 static void encodeSegment(R)(ref R dst, string segment) 1830 { 1831 static immutable char[16] digit = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; 1832 1833 foreach (char c; segment) { 1834 switch (c) { 1835 default: 1836 dst.put('%'); 1837 dst.put(digit[uint(c) / 16]); 1838 dst.put(digit[uint(c) % 16]); 1839 break; 1840 case 'a': .. case 'z': 1841 case 'A': .. case 'Z': 1842 case '0': .. case '9': 1843 case '-', '.', '_', '~': 1844 case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': 1845 case ':', '@': 1846 dst.put(c); 1847 break; 1848 } 1849 } 1850 } 1851 } 1852 1853 unittest { // regression tests 1854 assert(NativePath("").bySegment.empty); 1855 assert(NativePath("").bySegment2.empty); 1856 }