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