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 }