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