/** Contains routines for high level path handling. Copyright: © 2012-2021 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module vibe.core.path; import std.algorithm.searching : commonPrefix, endsWith, startsWith; import std.algorithm.comparison : equal, min; import std.algorithm.iteration : map; import std.exception : enforce; import std.range : empty, front, popFront, popFrontExactly, takeExactly; import std.range.primitives : ElementType, isInputRange, isOutputRange, isForwardRange, save; import std.traits : isArray, isInstanceOf, isSomeChar; import std.utf : byChar; /** Computes the relative path from `base_path` to this path. Params: path = The destination path base_path = The path from which the relative path starts See_also: `relativeToWeb` */ Path relativeTo(Path)(Path path, Path base_path) @safe if (isInstanceOf!(GenericPath, Path)) { import std.array : array, replicate; import std.range : chain, drop, take; assert(base_path.absolute, "Base path must be absolute for relativeTo."); assert(path.absolute, "Path must be absolute for relativeTo."); if (is(Path == WindowsPath)) { // FIXME: this shouldn't be a special case here! bool samePrefix(size_t n) { return path.bySegment2.map!(n => n.encodedName).take(n).equal(base_path.bySegment2.map!(n => n.encodedName).take(n)); } // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case auto pref = path.bySegment2; if (!pref.empty && pref.front.encodedName == "") { pref.popFront(); if (!pref.empty) { // different drive? if (pref.front.encodedName.endsWith(':') && !samePrefix(2)) return path; // different UNC path? if (pref.front.encodedName == "" && !samePrefix(4)) return path; } } } auto nodes = path.bySegment2; auto base_nodes = base_path.bySegment2; // skip and count common prefix size_t base = 0; while (!nodes.empty && !base_nodes.empty && equal(nodes.front.name, base_nodes.front.name)) { nodes.popFront(); base_nodes.popFront(); base++; } enum up = Path.Segment2("..", Path.defaultSeparator); auto ret = Path(base_nodes.map!(p => up).chain(nodes)); if (path.endsWithSlash) { if (ret.empty) return Path.fromTrustedString("." ~ path.toString()[$-1]); else ret.endsWithSlash = true; } return ret; } /// unittest { import std.array : array; import std.conv : to; assert(PosixPath("/some/path").relativeTo(PosixPath("/")) == PosixPath("some/path")); assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path/")) == PosixPath("../../path/")); assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path")) == PosixPath("../../path/")); assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("C:\\")) == WindowsPath("some\\path")); assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); assert(WindowsPath("\\\\server\\share\\some\\path").relativeTo(WindowsPath("\\\\server\\share\\")) == WindowsPath("some\\path")); assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("D:\\")) == WindowsPath("C:\\some\\path")); assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share")) == WindowsPath("C:\\some\\path\\")); assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("\\\\server\\some\\path\\")); assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("\\\\otherserver\\path")) == WindowsPath("\\\\server\\some\\path\\")); assert(WindowsPath("\\some\\path\\").relativeTo(WindowsPath("\\other\\path")) == WindowsPath("..\\..\\some\\path\\")); assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share\\path2")) == WindowsPath("..\\path1")); assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server2\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); } unittest { { auto parentpath = "/path/to/parent"; auto parentpathp = PosixPath(parentpath); auto subpath = "/path/to/parent/sub/"; auto subpathp = PosixPath(subpath); auto subpath_rel = "sub/"; assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); auto subfile = "/path/to/parent/child"; auto subfilep = PosixPath(subfile); auto subfile_rel = "child"; assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); } { // relative paths across Windows devices are not allowed auto p1 = WindowsPath("\\\\server\\share"); assert(p1.absolute); auto p2 = WindowsPath("\\\\server\\othershare"); assert(p2.absolute); auto p3 = WindowsPath("\\\\otherserver\\share"); assert(p3.absolute); auto p4 = WindowsPath("C:\\somepath"); assert(p4.absolute); auto p5 = WindowsPath("C:\\someotherpath"); assert(p5.absolute); auto p6 = WindowsPath("D:\\somepath"); assert(p6.absolute); auto p7 = WindowsPath("\\\\server\\share\\path"); assert(p7.absolute); auto p8 = WindowsPath("\\\\server\\share\\otherpath"); assert(p8.absolute); assert(p4.relativeTo(p5) == WindowsPath("..\\somepath")); assert(p4.relativeTo(p6) == WindowsPath("C:\\somepath")); assert(p4.relativeTo(p1) == WindowsPath("C:\\somepath")); assert(p1.relativeTo(p2) == WindowsPath("\\\\server\\share")); assert(p1.relativeTo(p3) == WindowsPath("\\\\server\\share")); assert(p1.relativeTo(p4) == WindowsPath("\\\\server\\share")); assert(p7.relativeTo(p1) == WindowsPath("path")); assert(p7.relativeTo(p8) == WindowsPath("..\\path")); } { // relative path, trailing slash auto p1 = PosixPath("/some/path"); auto p2 = PosixPath("/some/path/"); assert(p1.relativeTo(p1).toString() == ""); assert(p1.relativeTo(p2).toString() == ""); assert(p2.relativeTo(p2).toString() == "./"); } } nothrow unittest { auto p1 = PosixPath.fromTrustedString("/foo/bar/baz"); auto p2 = PosixPath.fromTrustedString("/foo/baz/bam"); assert(p2.relativeTo(p1).toString == "../../baz/bam"); } /** Computes the relative path to this path from `base_path` using web path rules. The difference to `relativeTo` is that a path not ending in a slash will not be considered as a path to a directory and the parent path will instead be used. Params: path = The destination path base_path = The path from which the relative path starts See_also: `relativeTo` */ Path relativeToWeb(Path)(Path path, Path base_path) @safe if (isInstanceOf!(GenericPath, Path)) { if (!base_path.endsWithSlash) { assert(base_path.absolute, "Base path must be absolute for relativeToWeb."); if (base_path.hasParentPath) base_path = base_path.parentPath; else base_path = Path("/"); assert(base_path.absolute); } return path.relativeTo(base_path); } /// /+unittest { assert(InetPath("/some/path").relativeToWeb(InetPath("/")) == InetPath("some/path")); assert(InetPath("/some/path/").relativeToWeb(InetPath("/some/other/path/")) == InetPath("../../path/")); assert(InetPath("/some/path/").relativeToWeb(InetPath("/some/other/path")) == InetPath("../path/")); }+/ /** Converts a path to its system native string representation. */ string toNativeString(P)(P path) { return (cast(NativePath)path).toString(); } /// Represents a path on Windows operating systems. alias WindowsPath = GenericPath!WindowsPathFormat; /// Represents a path on Unix/Posix systems. alias PosixPath = GenericPath!PosixPathFormat; /// Represents a path as part of an URI. alias InetPath = GenericPath!InetPathFormat; /// The path type native to the target operating system. version (Windows) alias NativePath = WindowsPath; else alias NativePath = PosixPath; deprecated("Use NativePath or one the specific path types instead.") alias Path = NativePath; deprecated("Use NativePath.Segment or one the specific path types instead.") alias PathEntry = Path.Segment; /// Provides a common interface to operate on paths of various kinds. struct GenericPath(F) { @safe: alias Format = F; /** A single path segment. */ static struct Segment { @safe: private { string m_name; char m_separator = 0; } /** Constructs a new path segment including an optional trailing separator. Params: name = The raw (unencoded) name of the path segment separator = Optional trailing path separator (e.g. `'/'`) Throws: A `PathValidationException` is thrown if the name contains characters that are invalid for the path type. In particular, any path separator characters may not be part of the name. */ this(string name, char separator = '\0') { import std.algorithm.searching : any; enforce!PathValidationException(separator == '\0' || Format.isSeparator(separator), "Invalid path separator."); auto err = Format.validateDecodedSegment(name); enforce!PathValidationException(err is null, err); m_name = name; m_separator = separator; } /** Constructs a path segment without performing validation. Note that in debug builds, there are still assertions in place that verify that the provided values are valid. Params: name = The raw (unencoded) name of the path segment separator = Optional trailing path separator (e.g. `'/'`) */ static Segment fromTrustedString(string name, char separator = '\0') nothrow @nogc pure { import std.algorithm.searching : any; assert(separator == '\0' || Format.isSeparator(separator)); assert(Format.validateDecodedSegment(name) is null, "Invalid path segment."); Segment ret; ret.m_name = name; ret.m_separator = separator; return ret; } deprecated("Use the constructor instead.") static Segment validateFilename(string name) { return Segment(name); } /// The (file/directory) name of the path segment. @property string name() const nothrow @nogc { return m_name; } /// The trailing separator (e.g. `'/'`) or `'\0'`. @property char separator() const nothrow @nogc { return m_separator; } /// ditto @property void separator(char ch) { enforce!PathValidationException(ch == '\0' || Format.isSeparator(ch), "Character is not a valid path separator."); m_separator = ch; } /// Returns `true` $(I iff) the segment has a trailing path separator. @property bool hasSeparator() const nothrow @nogc { return m_separator != '\0'; } deprecated("Use .name instead.") string toString() const nothrow @nogc { return m_name; } /** Converts the segment to another path type. The segment name will be re-validated during the conversion. The separator, if any, will be adopted or replaced by the default separator of the target path type. Throws: A `PathValidationException` is thrown if the segment name cannot be represented in the target path format. */ GenericPath!F.Segment opCast(T : GenericPath!F.Segment, F)() { char dsep = '\0'; if (m_separator) { if (F.isSeparator(m_separator)) dsep = m_separator; else dsep = F.defaultSeparator; } return GenericPath!F.Segment(m_name, dsep); } /// Compares two path segment names bool opEquals(Segment other) const nothrow @nogc { return this.name == other.name && this.hasSeparator == other.hasSeparator; } /// ditto bool opEquals(string name) const nothrow @nogc { return this.name == name; } } /** Represents a path as an forward range of `Segment`s. */ static struct PathRange { import std.traits : ReturnType; private { string m_path; Segment m_front; } private this(string path) { m_path = path; if (m_path.length) { auto ap = Format.getAbsolutePrefix(m_path); if (ap.length && !Format.isSeparator(ap[0])) m_front = Segment.fromTrustedString(null, '/'); else readFront(); } } @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment.init; } @property PathRange save() { return this; } @property Segment front() { return m_front; } void popFront() nothrow { assert(m_front != Segment.init); if (m_path.length) readFront(); else m_front = Segment.init; } private void readFront() nothrow { import std.array : array; auto n = Format.getFrontNode(m_path); m_path = m_path[n.length .. $]; char sep = '\0'; if (Format.isSeparator(n[$-1])) { sep = n[$-1]; n = n[0 .. $-1]; } static if (is(typeof(Format.decodeSingleSegment(n)) == string)) string ndec = Format.decodeSingleSegment(n); else string ndec = Format.decodeSingleSegment(n).array; m_front = Segment.fromTrustedString(ndec, sep); assert(m_front != Segment.init); } } /** A single path segment. */ static struct Segment2 { @safe: private { string m_encodedName; char m_separator = 0; } /** Constructs a new path segment including an optional trailing separator. Params: name = The raw (unencoded) name of the path segment separator = Optional trailing path separator (e.g. `'/'`) Throws: A `PathValidationException` is thrown if the name contains characters that are invalid for the path type. In particular, any path separator characters may not be part of the name. */ this(string name, char separator = '\0') { import std.algorithm.searching : any; enforce!PathValidationException(separator == '\0' || Format.isSeparator(separator), "Invalid path separator."); auto err = Format.validateDecodedSegment(name); enforce!PathValidationException(err is null, err); m_encodedName = Format.encodeSegment(name); m_separator = separator; } /** Constructs a path segment without performing validation. Note that in debug builds, there are still assertions in place that verify that the provided values are valid. Params: name = The raw (unencoded) name of the path segment separator = Optional trailing path separator (e.g. `'/'`) */ static Segment2 fromTrustedString(string name, char separator = '\0') nothrow pure { import std.algorithm.searching : any; assert(separator == '\0' || Format.isSeparator(separator)); assert(Format.validateDecodedSegment(name) is null, "Invalid path segment."); return fromTrustedEncodedString(Format.encodeSegment(name), separator); } /** Constructs a path segment without performing validation. Note that in debug builds, there are still assertions in place that verify that the provided values are valid. Params: encoded_name = The encoded name of the path segment separator = Optional trailing path separator (e.g. `'/'`) */ static Segment2 fromTrustedEncodedString(string encoded_name, char separator = '\0') nothrow @nogc pure { import std.algorithm.searching : any; import std.utf : byCodeUnit; assert(separator == '\0' || Format.isSeparator(separator)); assert(!encoded_name.byCodeUnit.any!(c => Format.isSeparator(c))); assert(Format.validatePath(encoded_name) is null, "Invalid path segment."); Segment2 ret; ret.m_encodedName = encoded_name; ret.m_separator = separator; return ret; } /** The (file/directory) name of the path segment. Note: Depending on the path type, this may return a generic range type instead of `string`. Use `name.to!string` in that case if you need an actual `string`. */ @property auto name() const nothrow @nogc { return Format.decodeSingleSegment(m_encodedName); } /// The encoded representation of the path segment name @property string encodedName() const nothrow @nogc { return m_encodedName; } /// The trailing separator (e.g. `'/'`) or `'\0'`. @property char separator() const nothrow @nogc { return m_separator; } /// ditto @property void separator(char ch) { enforce!PathValidationException(ch == '\0' || Format.isSeparator(ch), "Character is not a valid path separator."); m_separator = ch; } /// Returns `true` $(I iff) the segment has a trailing path separator. @property bool hasSeparator() const nothrow @nogc { return m_separator != '\0'; } /** The extension part of the file name. If the file name contains an extension, this returns a forward range with the extension including the leading dot. Otherwise an empty range is returned. See_also: `stripExtension` */ @property auto extension() const nothrow @nogc { return .extension(this.name); } /// unittest { assert(PosixPath("/foo/bar.txt").head2.extension.equal(".txt")); assert(PosixPath("/foo/bar").head2.extension.equal("")); assert(PosixPath("/foo/.bar").head2.extension.equal("")); assert(PosixPath("/foo/.bar.txt").head2.extension.equal(".txt")); } /** Returns the file base name, excluding the extension. See_also: `extension` */ @property auto withoutExtension() const nothrow @nogc { return .stripExtension(this.name); } /// unittest { assert(PosixPath("/foo/bar.txt").head2.withoutExtension.equal("bar")); assert(PosixPath("/foo/bar").head2.withoutExtension.equal("bar")); assert(PosixPath("/foo/.bar").head2.withoutExtension.equal(".bar")); assert(PosixPath("/foo/.bar.txt").head2.withoutExtension.equal(".bar")); } /** Converts the segment to another path type. The segment name will be re-validated during the conversion. The separator, if any, will be adopted or replaced by the default separator of the target path type. Throws: A `PathValidationException` is thrown if the segment name cannot be represented in the target path format. */ GenericPath!F.Segment2 opCast(T : GenericPath!F.Segment2, F)() { import std.array : array; char dsep = '\0'; if (m_separator) { if (F.isSeparator(m_separator)) dsep = m_separator; else dsep = F.defaultSeparator; } static if (is(typeof(this.name) == string)) string n = this.name; else string n = this.name.array; return GenericPath!F.Segment2(n, dsep); } /// Compares two path segment names bool opEquals(Segment2 other) const nothrow @nogc { try return equal(this.name, other.name) && this.hasSeparator == other.hasSeparator; catch (Exception e) assert(false, e.msg); } /// ditto bool opEquals(string name) const nothrow @nogc { import std.utf : byCodeUnit; try return equal(this.name, name.byCodeUnit); catch (Exception e) assert(false, e.msg); } } private { string m_path; } /// The default path segment separator character. enum char defaultSeparator = Format.defaultSeparator; /** Constructs a path from its string representation. Throws: A `PathValidationException` is thrown if the given path string is not valid. */ this(string p) { auto err = Format.validatePath(p); enforce!PathValidationException(err is null, err); m_path = p; } /** Constructs a path from a single path segment. This is equivalent to calling the range based constructor with a single-element range. */ this(Segment segment) { import std.range : only; this(only(segment)); } /// ditto this(Segment2 segment) { import std.range : only; this(only(segment)); } /** Constructs a path from an input range of `Segment`s. Throws: Since path segments are pre-validated, this constructor does not throw an exception. */ this(R)(R segments) if (isInputRange!R && is(ElementType!R : Segment)) { import std.array : appender; auto dst = appender!string; Format.toString(segments, dst); m_path = dst.data; } /// ditto this(R)(R segments) if (isInputRange!R && is(ElementType!R : Segment2)) { import std.array : appender; auto dst = appender!string; Format.toString(segments, dst); m_path = dst.data; } /** Constructs a path from its string representation. This is equivalent to calling the string based constructor. */ static GenericPath fromString(string p) { return GenericPath(p); } /** Constructs a path from its string representation, skipping the validation. Note that it is required to pass a pre-validated path string to this function. Debug builds will enforce this with an assertion. */ static GenericPath fromTrustedString(string p) nothrow @nogc { assert(Format.validatePath(p) is null, "Invalid trusted path."); GenericPath ret; ret.m_path = p; return ret; } /// Tests if a certain character is a path segment separator. static bool isSeparator(dchar ch) { return ch < 0x80 && Format.isSeparator(cast(char)ch); } /// Tests if the path is represented by an empty string. @property bool empty() const nothrow @nogc { return m_path.length == 0; } /// Tests if the path is absolute. @property bool absolute() const nothrow @nogc { return Format.getAbsolutePrefix(m_path).length > 0; } /// Determines whether the path ends with a path separator (i.e. represents a folder specifically). @property bool endsWithSlash() const nothrow @nogc { return m_path.length > 0 && Format.isSeparator(m_path[$-1]); } /// ditto @property void endsWithSlash(bool v) nothrow { bool ews = this.endsWithSlash; if (!ews && v) m_path ~= Format.defaultSeparator; else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME?: "/test//" -> "/test/" } /// Iterates over the path by `Segment`. @property PathRange bySegment() const { return PathRange(m_path); } /** Iterates over the individual segments of the path. Returns a forward range of `Segment2`s. */ @property auto bySegment2() const { static struct R { import std.traits : ReturnType; private { string m_path; Segment2 m_front; } private this(string path) { m_path = path; if (m_path.length) { auto ap = Format.getAbsolutePrefix(m_path); if (ap.length && !Format.isSeparator(ap[0])) m_front = Segment2.fromTrustedEncodedString(null, '/'); else readFront(); } } @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment2.init; } @property R save() { return this; } @property Segment2 front() { return m_front; } void popFront() nothrow { assert(m_front != Segment2.init); if (m_path.length) readFront(); else m_front = Segment2.init; } private void readFront() { auto n = Format.getFrontNode(m_path); m_path = m_path[n.length .. $]; char sep = '\0'; if (Format.isSeparator(n[$-1])) { sep = n[$-1]; n = n[0 .. $-1]; } m_front = Segment2.fromTrustedEncodedString(n, sep); assert(m_front != Segment2.init); } } return R(m_path); } /// unittest { InetPath p = "foo/bar/baz"; assert(p.bySegment2.equal([ InetPath.Segment2("foo", '/'), InetPath.Segment2("bar", '/'), InetPath.Segment2("baz") ])); } /** Iterates over the path by segment, each time returning the sub path leading to that segment. */ @property auto byPrefix() const nothrow @nogc { static struct R { import std.traits : ReturnType; private { string m_path; string m_remainder; } private this(string path) { m_path = path; m_remainder = path; if (m_path.length) { auto ap = Format.getAbsolutePrefix(m_path); if (ap.length && !Format.isSeparator(ap[0])) m_remainder = m_remainder[ap.length .. $]; else popFront(); } } @property bool empty() const nothrow @nogc { return m_path.length == 0; } @property R save() { return this; } @property GenericPath front() { return GenericPath.fromTrustedString(m_path[0 .. $-m_remainder.length]); } void popFront() nothrow { assert(m_remainder.length > 0 || m_path.length > 0); if (m_remainder.length) readFront(); else m_path = ""; } private void readFront() { auto n = Format.getFrontNode(m_remainder); m_remainder = m_remainder[n.length .. $]; } } return R(m_path); } /// unittest { assert(InetPath("foo/bar/baz").byPrefix .equal([ InetPath("foo/"), InetPath("foo/bar/"), InetPath("foo/bar/baz") ])); assert(InetPath("/foo/bar").byPrefix .equal([ InetPath("/"), InetPath("/foo/"), InetPath("/foo/bar"), ])); } /// Returns the trailing segment of the path. @property Segment head() const { import std.array : array; auto n = Format.getBackNode(m_path); char sep = '\0'; if (n.length > 0 && Format.isSeparator(n[$-1])) { sep = n[$-1]; n = n[0 .. $-1]; } static if (is(typeof(Format.decodeSingleSegment(n)) == string)) string ndec = Format.decodeSingleSegment(n); else string ndec = Format.decodeSingleSegment(n).array; return Segment.fromTrustedString(ndec, sep); } /// Returns the trailing segment of the path. @property Segment2 head2() const @nogc { auto n = Format.getBackNode(m_path); char sep = '\0'; if (n.length > 0 && Format.isSeparator(n[$-1])) { sep = n[$-1]; n = n[0 .. $-1]; } return Segment2.fromTrustedEncodedString(n, sep); } /** Determines if the `parentPath` property is valid. */ @property bool hasParentPath() const @nogc { auto b = Format.getBackNode(m_path); return b.length < m_path.length; } /** Returns a prefix of this path, where the last segment has been dropped. Throws: An `Exception` is thrown if this path has no parent path. Use `hasParentPath` to test this upfront. */ @property GenericPath parentPath() const @nogc { auto b = Format.getBackNode(m_path); static immutable Exception e = new Exception("Path has no parent path"); if (b.length >= m_path.length) throw e; return GenericPath.fromTrustedString(m_path[0 .. $ - b.length]); } /** The extension part of the file name pointed to by the path. If the path is not empty and its head segment has an extension, this returns a forward range with the extension including the leading dot. Otherwise an empty range is returned. See `Segment2.extension` for a full description. See_also: `Segment2.extension`, `Segment2.stripExtension` */ @property auto fileExtension() const nothrow @nogc { if (this.empty) return typeof(this.head2.extension).init; return this.head2.extension; } /** Returns the normalized form of the path. See `normalize` for a full description. */ @property GenericPath normalized() const { GenericPath ret = this; ret.normalize(); return ret; } unittest { assert(PosixPath("foo/../bar").normalized == PosixPath("bar")); assert(PosixPath("foo//./bar/../baz").normalized == PosixPath("foo/baz")); } /** Removes any redundant path segments and replaces all separators by the default one. The resulting path representation is suitable for basic semantic comparison to other normalized paths. Note that there are still ways for different normalized paths to represent the same file. Examples of this are the tilde shortcut to the home directory on Unix and Linux operating systems, symbolic or hard links, and possibly environment variables are examples of this. Throws: Throws an `Exception` if an absolute path contains parent directory segments ("..") that lead to a path that is a parent path of the root path. */ void normalize() { import std.array : appender, join; Segment2[] newnodes; bool got_non_sep = false; foreach (n; this.bySegment2) { if (n.hasSeparator) n.separator = Format.defaultSeparator; if (!got_non_sep) { if (n.encodedName == "") newnodes ~= n; else got_non_sep = true; } switch (n.encodedName) { default: newnodes ~= n; break; case "", ".": break; case "..": enforce(!this.absolute || newnodes.length > 0, "Path goes below root node."); if (newnodes.length > 0 && newnodes[$-1].encodedName != "..") newnodes = newnodes[0 .. $-1]; else newnodes ~= n; break; } } auto dst = appender!string; Format.toString(newnodes, dst); m_path = dst.data; } /// unittest { auto path = WindowsPath("C:\\test/foo/./bar///../baz"); path.normalize(); assert(path.toString() == "C:\\test\\foo\\baz", path.toString()); path = WindowsPath("foo/../../bar/"); path.normalize(); assert(path.toString() == "..\\bar\\"); } /// Returns the string representation of the path. string toString() const nothrow @nogc { return m_path; } /// Computes a hash sum, enabling storage within associative arrays. size_t toHash() const nothrow @trusted { try return typeid(string).getHash(&m_path); catch (Exception e) assert(false, "getHash for string throws!?"); } /** Compares two path objects. Note that the exact string representation of the two paths will be compared. To get a basic semantic comparison, the paths must be normalized first. */ bool opEquals(GenericPath other) const @nogc { return this.m_path == other.m_path; } /** Converts the path to a different path format. Throws: A `PathValidationException` will be thrown if the path is not representable in the requested path format. This can happen especially when converting Posix or Internet paths to windows paths, since Windows paths cannot contain a number of characters that the other representations can, in theory. */ P opCast(P)() const if (isInstanceOf!(.GenericPath, P)) { static if (is(P == GenericPath)) return this; else return P(this.bySegment2.map!(n => cast(P.Segment2)n)); } /** Concatenates two paths. The right hand side must represent a relative path. */ GenericPath opBinary(string op : "~")(string subpath) const { return this ~ GenericPath(subpath); } /// ditto GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); } /// ditto GenericPath opBinary(string op : "~")(Segment2 subpath) const { return this ~ GenericPath(subpath); } /// ditto GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment subpath) const { return this ~ cast(Segment)(subpath); } /// ditto GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment2 subpath) const { return this ~ cast(Segment2)(subpath); } /// ditto GenericPath opBinary(string op : "~")(GenericPath subpath) const nothrow { assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path."); if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path); else return GenericPath.fromTrustedString(m_path ~ Format.defaultSeparator ~ subpath.m_path); } /// ditto GenericPath opBinary(string op : "~", F)(GenericPath!F subpath) const if (!is(F == Format)) { return this ~ cast(GenericPath)subpath; } /// ditto GenericPath opBinary(string op : "~", R)(R entries) const nothrow if (isInputRange!R && is(ElementType!R : Segment)) { return this ~ GenericPath(entries); } /// ditto GenericPath opBinary(string op : "~", R)(R entries) const nothrow if (isInputRange!R && is(ElementType!R : Segment2)) { return this ~ GenericPath(entries); } /// Appends a relative path to this path. void opOpAssign(string op : "~", T)(T op) { this = this ~ op; } /** Tests whether the given path is a prefix of this path. Any path separators will be ignored during the comparison. */ bool startsWith(GenericPath prefix) const nothrow { return bySegment2.map!(n => n.name).startsWith(prefix.bySegment2.map!(n => n.name)); } } unittest { assert(PosixPath("hello/world").bySegment.equal([PosixPath.Segment("hello",'/'), PosixPath.Segment("world")])); assert(PosixPath("/hello/world/").bySegment.equal([PosixPath.Segment("",'/'), PosixPath.Segment("hello",'/'), PosixPath.Segment("world",'/')])); assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")])); assert(WindowsPath("hello/world").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world")])); assert(WindowsPath("/hello/world/").bySegment.equal([WindowsPath.Segment("",'/'), WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world",'/')])); assert(WindowsPath("hello\\w/orld").bySegment.equal([WindowsPath.Segment("hello",'\\'), WindowsPath.Segment("w",'/'), WindowsPath.Segment("orld")])); assert(WindowsPath("hello/w\\orld").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("w",'\\'), WindowsPath.Segment("orld")])); } unittest { assert(PosixPath("hello/world").bySegment2.equal([PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world")])); assert(PosixPath("/hello/world/").bySegment2.equal([PosixPath.Segment2("",'/'), PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world",'/')])); assert(PosixPath("hello\\world").bySegment2.equal([PosixPath.Segment2("hello\\world")])); assert(WindowsPath("hello/world").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world")])); assert(WindowsPath("/hello/world/").bySegment2.equal([WindowsPath.Segment2("",'/'), WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world",'/')])); assert(WindowsPath("hello\\w/orld").bySegment2.equal([WindowsPath.Segment2("hello",'\\'), WindowsPath.Segment2("w",'/'), WindowsPath.Segment2("orld")])); assert(WindowsPath("hello/w\\orld").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("w",'\\'), WindowsPath.Segment2("orld")])); assert(PosixPath("hello/world").byPrefix.equal([PosixPath("hello/"), PosixPath("hello/world")])); assert(PosixPath("/hello/world/").byPrefix.equal([PosixPath("/"), PosixPath("/hello/"), PosixPath("/hello/world/")])); assert(WindowsPath("C:\\Windows").byPrefix.equal([WindowsPath("C:\\"), WindowsPath("C:\\Windows")])); } unittest { { auto unc = "\\\\server\\share\\path"; auto uncp = WindowsPath(unc); assert(uncp.absolute); uncp.normalize(); version(Windows) assert(uncp.toNativeString() == unc); assert(uncp.absolute); assert(!uncp.endsWithSlash); } { auto abspath = "/test/path/"; auto abspathp = PosixPath(abspath); assert(abspathp.toString() == abspath); version(Windows) {} else assert(abspathp.toNativeString() == abspath); assert(abspathp.absolute); assert(abspathp.endsWithSlash); alias S = PosixPath.Segment; assert(abspathp.bySegment.equal([S("", '/'), S("test", '/'), S("path", '/')])); } { auto relpath = "test/path/"; auto relpathp = PosixPath(relpath); assert(relpathp.toString() == relpath); version(Windows) assert(relpathp.toNativeString() == "test/path/"); else assert(relpathp.toNativeString() == relpath); assert(!relpathp.absolute); assert(relpathp.endsWithSlash); alias S = PosixPath.Segment; assert(relpathp.bySegment.equal([S("test", '/'), S("path", '/')])); } { auto winpath = "C:\\windows\\test"; auto winpathp = WindowsPath(winpath); assert(winpathp.toString() == "C:\\windows\\test"); assert((cast(PosixPath)winpathp).toString() == "/C:/windows/test", (cast(PosixPath)winpathp).toString()); version(Windows) assert(winpathp.toNativeString() == winpath); else assert(winpathp.toNativeString() == "/C:/windows/test"); assert(winpathp.absolute); assert(!winpathp.endsWithSlash); alias S = WindowsPath.Segment; assert(winpathp.bySegment.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")])); } { auto dotpath = "/test/../test2/././x/y"; auto dotpathp = PosixPath(dotpath); assert(dotpathp.toString() == "/test/../test2/././x/y"); dotpathp.normalize(); assert(dotpathp.toString() == "/test2/x/y", dotpathp.toString()); } { auto dotpath = "/test/..////test2//./x/y"; auto dotpathp = PosixPath(dotpath); assert(dotpathp.toString() == "/test/..////test2//./x/y"); dotpathp.normalize(); assert(dotpathp.toString() == "/test2/x/y"); } assert(WindowsPath("C:\\Windows").absolute); assert((cast(InetPath)WindowsPath("C:\\Windows")).toString() == "/C:/Windows"); assert((WindowsPath("C:\\Windows") ~ InetPath("test/this")).toString() == "C:\\Windows\\test/this"); assert(InetPath("/C:/Windows").absolute); assert((cast(WindowsPath)InetPath("/C:/Windows")).toString() == "C:/Windows"); assert((InetPath("/C:/Windows") ~ WindowsPath("test\\this")).toString() == "/C:/Windows/test/this"); assert((InetPath("") ~ WindowsPath("foo\\bar")).toString() == "foo/bar"); assert((cast(InetPath)WindowsPath("C:\\Windows\\")).toString() == "/C:/Windows/"); assert(NativePath("").empty); assert(PosixPath("/") ~ NativePath("foo/bar") == PosixPath("/foo/bar")); assert(PosixPath("") ~ NativePath("foo/bar") == PosixPath("foo/bar")); assert(PosixPath("foo") ~ NativePath("bar") == PosixPath("foo/bar")); assert(PosixPath("foo/") ~ NativePath("bar") == PosixPath("foo/bar")); } unittest { { auto unc = "\\\\server\\share\\path"; auto uncp = WindowsPath(unc); assert(uncp.absolute); uncp.normalize(); version(Windows) assert(uncp.toNativeString() == unc); assert(uncp.absolute); assert(!uncp.endsWithSlash); } { auto abspath = "/test/path/"; auto abspathp = PosixPath(abspath); assert(abspathp.toString() == abspath); version(Windows) {} else assert(abspathp.toNativeString() == abspath); assert(abspathp.absolute); assert(abspathp.endsWithSlash); alias S = PosixPath.Segment2; assert(abspathp.bySegment2.equal([S("", '/'), S("test", '/'), S("path", '/')])); } { auto relpath = "test/path/"; auto relpathp = PosixPath(relpath); assert(relpathp.toString() == relpath); version(Windows) assert(relpathp.toNativeString() == "test/path/"); else assert(relpathp.toNativeString() == relpath); assert(!relpathp.absolute); assert(relpathp.endsWithSlash); alias S = PosixPath.Segment2; assert(relpathp.bySegment2.equal([S("test", '/'), S("path", '/')])); } { auto winpath = "C:\\windows\\test"; auto winpathp = WindowsPath(winpath); assert(winpathp.toString() == "C:\\windows\\test"); assert((cast(PosixPath)winpathp).toString() == "/C:/windows/test", (cast(PosixPath)winpathp).toString()); version(Windows) assert(winpathp.toNativeString() == winpath); else assert(winpathp.toNativeString() == "/C:/windows/test"); assert(winpathp.absolute); assert(!winpathp.endsWithSlash); alias S = WindowsPath.Segment2; assert(winpathp.bySegment2.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")])); } } @safe unittest { import std.array : appender; auto app = appender!(PosixPath[]); void test1(PosixPath p) { app.put(p); } void test2(PosixPath[] ps) { app.put(ps); } //void test3(const(PosixPath) p) { app.put(p); } // DMD issue 17251 //void test4(const(PosixPath)[] ps) { app.put(ps); } } unittest { import std.exception : assertThrown, assertNotThrown; assertThrown!PathValidationException(WindowsPath.Segment("foo/bar")); assertThrown!PathValidationException(PosixPath.Segment("foo/bar")); assertNotThrown!PathValidationException(InetPath.Segment("foo/bar")); auto p = InetPath("/foo%2fbar/"); assert(p.bySegment.equal([InetPath.Segment("",'/'), InetPath.Segment("foo/bar",'/')])); p ~= InetPath.Segment("baz/bam"); assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); } unittest { import std.exception : assertThrown, assertNotThrown; assertThrown!PathValidationException(WindowsPath.Segment2("foo/bar")); assertThrown!PathValidationException(PosixPath.Segment2("foo/bar")); assertNotThrown!PathValidationException(InetPath.Segment2("foo/bar")); auto p = InetPath("/foo%2fbar/"); import std.conv : to; assert(p.bySegment2.equal([InetPath.Segment2("",'/'), InetPath.Segment2("foo/bar",'/')]), p.bySegment2.to!string); p ~= InetPath.Segment2("baz/bam"); assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); } unittest { assert(!PosixPath("").hasParentPath); assert(!PosixPath("/").hasParentPath); assert(!PosixPath("foo\\bar").hasParentPath); assert(PosixPath("foo/bar").parentPath.toString() == "foo/"); assert(PosixPath("./foo").parentPath.toString() == "./"); assert(PosixPath("./foo").parentPath.toString() == "./"); assert(!WindowsPath("").hasParentPath); assert(!WindowsPath("/").hasParentPath); assert(WindowsPath("foo\\bar").parentPath.toString() == "foo\\"); assert(WindowsPath("foo/bar").parentPath.toString() == "foo/"); assert(WindowsPath("./foo").parentPath.toString() == "./"); assert(WindowsPath("./foo").parentPath.toString() == "./"); assert(!InetPath("").hasParentPath); assert(!InetPath("/").hasParentPath); assert(InetPath("foo/bar").parentPath.toString() == "foo/"); assert(InetPath("foo/bar%2Fbaz").parentPath.toString() == "foo/"); assert(InetPath("./foo").parentPath.toString() == "./"); assert(InetPath("./foo").parentPath.toString() == "./"); } unittest { assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar"); } unittest { assert(WindowsPath([WindowsPath.Segment2("foo"), WindowsPath.Segment2("bar")]).toString() == "foo\\bar"); } /// Thrown when an invalid string representation of a path is detected. class PathValidationException : Exception { this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null) pure nothrow @nogc @safe { super(text, file, line, next); } } /** Implements Windows path semantics. See_also: `WindowsPath` */ struct WindowsPathFormat { static void toString(I, O)(I segments, O dst) if (isInputRange!I && isOutputRange!(O, char)) { char sep(char s) { return isSeparator(s) ? s : defaultSeparator; } if (segments.empty) return; if (segments.front.name == "" && segments.front.separator) { auto s = segments.front.separator; segments.popFront(); if (segments.empty || !segments.front.name.endsWith(":")) dst.put(sep(s)); } char lastsep = '\0'; bool first = true; foreach (s; segments) { if (!first || lastsep) dst.put(sep(lastsep)); else first = false; dst.put(s.name); lastsep = s.separator; } if (lastsep) dst.put(sep(lastsep)); } unittest { import std.array : appender; struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } assert(str() == ""); assert(str(Segment("",'/')) == "/"); assert(str(Segment("",'/'), Segment("foo")) == "/foo"); assert(str(Segment("",'\\')) == "\\"); assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); assert(str(Segment("",'\\'), Segment("foo",'\\')) == "\\foo\\"); assert(str(Segment("f oo")) == "f oo"); assert(str(Segment("",'\\'), Segment("C:")) == "C:"); assert(str(Segment("",'\\'), Segment("C:", '/')) == "C:/"); assert(str(Segment("foo",'\\'), Segment("C:")) == "foo\\C:"); assert(str(Segment("foo"), Segment("bar")) == "foo\\bar"); } @safe nothrow pure: enum defaultSeparator = '\\'; static bool isSeparator(dchar ch) @nogc { return ch == '\\' || ch == '/'; } static string getAbsolutePrefix(string path) @nogc { if (!path.length) return null; if (isSeparator(path[0])) { return path[0 .. 1]; } foreach (i; 1 .. path.length) if (isSeparator(path[i])) { if (path[i-1] == ':') return path[0 .. i+1]; break; } return path[$-1] == ':' ? path : null; } unittest { assert(getAbsolutePrefix("test") == ""); assert(getAbsolutePrefix("test/") == ""); assert(getAbsolutePrefix("/test") == "/"); assert(getAbsolutePrefix("\\test") == "\\"); assert(getAbsolutePrefix("C:\\") == "C:\\"); assert(getAbsolutePrefix("C:") == "C:"); assert(getAbsolutePrefix("C:\\test") == "C:\\"); assert(getAbsolutePrefix("C:\\test\\") == "C:\\"); assert(getAbsolutePrefix("C:/") == "C:/"); assert(getAbsolutePrefix("C:/test") == "C:/"); assert(getAbsolutePrefix("C:/test/") == "C:/"); assert(getAbsolutePrefix("\\\\server") == "\\"); assert(getAbsolutePrefix("\\\\server\\") == "\\"); assert(getAbsolutePrefix("\\\\.\\") == "\\"); assert(getAbsolutePrefix("\\\\?\\") == "\\"); } static string getFrontNode(string path) @nogc { foreach (i; 0 .. path.length) if (isSeparator(path[i])) return path[0 .. i+1]; return path; } unittest { assert(getFrontNode("") == ""); assert(getFrontNode("/bar") == "/"); assert(getFrontNode("foo/bar") == "foo/"); assert(getFrontNode("foo/") == "foo/"); assert(getFrontNode("foo") == "foo"); assert(getFrontNode("\\bar") == "\\"); assert(getFrontNode("foo\\bar") == "foo\\"); assert(getFrontNode("foo\\") == "foo\\"); } static string getBackNode(string path) @nogc { if (!path.length) return path; foreach_reverse (i; 0 .. path.length-1) if (isSeparator(path[i])) return path[i+1 .. $]; return path; } unittest { assert(getBackNode("") == ""); assert(getBackNode("/bar") == "bar"); assert(getBackNode("foo/bar") == "bar"); assert(getBackNode("foo/") == "foo/"); assert(getBackNode("foo") == "foo"); assert(getBackNode("\\bar") == "bar"); assert(getBackNode("foo\\bar") == "bar"); assert(getBackNode("foo\\") == "foo\\"); } deprecated("Use decodeSingleSegment instead.") static auto decodeSegment(S)(string segment) { static struct R { S[2] items; size_t i = items.length; this(S s) { i = 1; items[i] = s; } this(S a, S b) { i = 0; items[0] = a; items[1] = b; } @property ref S front() { return items[i]; } @property bool empty() const { return i >= items.length; } void popFront() { i++; } } assert(segment.length > 0, "Path segment string must not be empty."); char sep = '\0'; if (!segment.length) return R(S.fromTrustedString(null)); if (isSeparator(segment[$-1])) { sep = segment[$-1]; segment = segment[0 .. $-1]; } // output an absolute marker segment for "C:\" style absolute segments if (segment.length > 0 && segment[$-1] == ':') return R(S.fromTrustedString("", '/'), S.fromTrustedString(segment, sep)); return R(S.fromTrustedString(segment, sep)); } deprecated unittest { struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} assert(decodeSegment!Segment("foo").equal([Segment("foo")])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo%20o", '\\')])); assert(decodeSegment!Segment("C:\\").equal([Segment("",'/'), Segment("C:", '\\')])); assert(decodeSegment!Segment("bar:\\").equal([Segment("",'/'), Segment("bar:", '\\')])); } static string decodeSingleSegment(string segment) @nogc { assert(segment.length == 0 || segment[$-1] != '/'); return segment; } unittest { struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} assert(decodeSingleSegment("foo") == "foo"); assert(decodeSingleSegment("fo%20o") == "fo%20o"); assert(decodeSingleSegment("C:") == "C:"); assert(decodeSingleSegment("bar:") == "bar:"); } static string validatePath(string path) @nogc { import std.algorithm.comparison : among; // skip UNC prefix if (path.startsWith("\\\\")) { path = path[2 .. $]; while (path.length && !isSeparator(path[0])) { if (path[0] < 32 || path[0].among('<', '>', '|')) return "Invalid character in UNC host name."; path = path[1 .. $]; } if (path.length) path = path[1 .. $]; } // stricter validation for the rest bool had_sep = false; foreach (i, char c; path) { if (c < 32 || c.among!('<', '>', '|', '?')) return "Invalid character in path."; if (isSeparator(c)) had_sep = true; else if (c == ':' && (had_sep || i+1 < path.length && !isSeparator(path[i+1]))) return "Colon in path that is not part of a drive name."; } return null; } static string validateDecodedSegment(string segment) @nogc { auto pe = validatePath(segment); if (pe) return pe; foreach (char c; segment) if (isSeparator(c)) return "Path segment contains separator character."; return null; } unittest { assert(validatePath("c:\\foo") is null); assert(validatePath("\\\\?\\c:\\foo") is null); assert(validatePath("//?\\c:\\foo") !is null); assert(validatePath("-foo/bar\\*\\baz") is null); assert(validatePath("foo\0bar") !is null); assert(validatePath("foo\tbar") !is null); assert(validatePath("\\c:\\foo") !is null); assert(validatePath("c:d\\foo") !is null); assert(validatePath("foo\\b:ar") !is null); assert(validatePath("foo\\bar:\\baz") !is null); } static string encodeSegment(string segment) { assert(segment.length == 0 || segment[$-1] != '/'); return segment; } } /** Implements Unix/Linux path semantics. See_also: `WindowsPath` */ struct PosixPathFormat { static void toString(I, O)(I segments, O dst) { char lastsep = '\0'; bool first = true; foreach (s; segments) { if (!first || lastsep) dst.put('/'); else first = false; dst.put(s.name); lastsep = s.separator; } if (lastsep) dst.put('/'); } unittest { import std.array : appender; struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } assert(str() == ""); assert(str(Segment("",'/')) == "/"); assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); assert(str(Segment("f oo")) == "f oo"); assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); } @safe nothrow pure: enum defaultSeparator = '/'; static bool isSeparator(dchar ch) @nogc { return ch == '/'; } static string getAbsolutePrefix(string path) @nogc { if (path.length > 0 && path[0] == '/') return path[0 .. 1]; return null; } unittest { assert(getAbsolutePrefix("/") == "/"); assert(getAbsolutePrefix("/test") == "/"); assert(getAbsolutePrefix("/test/") == "/"); assert(getAbsolutePrefix("test/") == ""); assert(getAbsolutePrefix("") == ""); assert(getAbsolutePrefix("./") == ""); } static string getFrontNode(string path) @nogc { import std.string : indexOf; auto idx = path.indexOf('/'); return idx < 0 ? path : path[0 .. idx+1]; } unittest { assert(getFrontNode("") == ""); assert(getFrontNode("/bar") == "/"); assert(getFrontNode("foo/bar") == "foo/"); assert(getFrontNode("foo/") == "foo/"); assert(getFrontNode("foo") == "foo"); } static string getBackNode(string path) @nogc { if (!path.length) return path; foreach_reverse (i; 0 .. path.length-1) if (path[i] == '/') return path[i+1 .. $]; return path; } unittest { assert(getBackNode("") == ""); assert(getBackNode("/bar") == "bar"); assert(getBackNode("foo/bar") == "bar"); assert(getBackNode("foo/") == "foo/"); assert(getBackNode("foo") == "foo"); } static string validatePath(string path) @nogc { foreach (char c; path) if (c == '\0') return "Invalid NUL character in file name"; return null; } static string validateDecodedSegment(string segment) @nogc { auto pe = validatePath(segment); if (pe) return pe; foreach (char c; segment) if (isSeparator(c)) return "Path segment contains separator character."; return null; } unittest { assert(validatePath("-foo/bar*/baz?") is null); assert(validatePath("foo\0bar") !is null); } deprecated("Use decodeSingleSegment instead.") static auto decodeSegment(S)(string segment) { assert(segment.length > 0, "Path segment string must not be empty."); import std.range : only; if (!segment.length) return only(S.fromTrustedString(null, '/')); if (segment[$-1] == '/') return only(S.fromTrustedString(segment[0 .. $-1], '/')); return only(S.fromTrustedString(segment)); } deprecated unittest { struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} assert(decodeSegment!Segment("foo").equal([Segment("foo")])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo%20o\\")])); } static string decodeSingleSegment(string segment) @nogc { assert(segment.length == 0 || segment[$-1] != '/'); return segment; } unittest { struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} assert(decodeSingleSegment("foo") == "foo"); assert(decodeSingleSegment("fo%20o\\") == "fo%20o\\"); } static string encodeSegment(string segment) { assert(segment.length == 0 || segment[$-1] != '/'); return segment; } } /** Implements URI/Internet path semantics. See_also: `WindowsPath` */ struct InetPathFormat { static void toString(I, O)(I segments, O dst) { char lastsep = '\0'; bool first = true; foreach (e; segments) { if (!first || lastsep) dst.put('/'); else first = false; static if (is(typeof(e.encodedName))) dst.put(e.encodedName); else encodeSegment(dst, e.name); lastsep = e.separator; } if (lastsep) dst.put('/'); } unittest { import std.array : appender; struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } assert(str() == ""); assert(str(Segment("",'/')) == "/"); assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); assert(str(Segment("f oo")) == "f%20oo"); assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); } @safe pure nothrow: enum defaultSeparator = '/'; static bool isSeparator(dchar ch) @nogc { return ch == '/'; } static string getAbsolutePrefix(string path) @nogc { if (path.length > 0 && path[0] == '/') return path[0 .. 1]; return null; } unittest { assert(getAbsolutePrefix("/") == "/"); assert(getAbsolutePrefix("/test") == "/"); assert(getAbsolutePrefix("/test/") == "/"); assert(getAbsolutePrefix("test/") == ""); assert(getAbsolutePrefix("") == ""); assert(getAbsolutePrefix("./") == ""); } static string getFrontNode(string path) @nogc { import std.string : indexOf; auto idx = path.indexOf('/'); return idx < 0 ? path : path[0 .. idx+1]; } unittest { assert(getFrontNode("") == ""); assert(getFrontNode("/bar") == "/"); assert(getFrontNode("foo/bar") == "foo/"); assert(getFrontNode("foo/") == "foo/"); assert(getFrontNode("foo") == "foo"); } static string getBackNode(string path) @nogc { import std.string : lastIndexOf; if (!path.length) return path; ptrdiff_t idx; try idx = path[0 .. $-1].lastIndexOf('/'); catch (Exception e) assert(false, e.msg); if (idx >= 0) return path[idx+1 .. $]; return path; } unittest { assert(getBackNode("") == ""); assert(getBackNode("/bar") == "bar"); assert(getBackNode("foo/bar") == "bar"); assert(getBackNode("foo/") == "foo/"); assert(getBackNode("foo") == "foo"); } static string validatePath(string path) @nogc { for (size_t i = 0; i < path.length; i++) { if (isAsciiAlphaNum(path[i])) continue; switch (path[i]) { default: return "Invalid character in internet path."; // unreserved case '-', '.', '_', '~': // subdelims case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // additional delims case ':', '@': // segment delimiter case '/': break; case '%': // pct encoding if (path.length < i+3) return "Unterminated percent encoding sequence in internet path."; foreach (j; 0 .. 2) { switch (path[++i]) { default: return "Invalid percent encoding sequence in internet path."; case '0': .. case '9': case 'a': .. case 'f': case 'A': .. case 'F': break; } } break; } } return null; } static string validateDecodedSegment(string seg) @nogc { return null; } unittest { assert(validatePath("") is null); assert(validatePath("/") is null); assert(validatePath("/test") is null); assert(validatePath("test") is null); assert(validatePath("/C:/test") is null); assert(validatePath("/test%ab") is null); assert(validatePath("/test%ag") !is null); assert(validatePath("/test%a") !is null); assert(validatePath("/test%") !is null); assert(validatePath("/test§") !is null); assert(validatePath("föö") !is null); } deprecated("Use decodeSingleSegment instead.") static auto decodeSegment(S)(string segment) { import std.algorithm.searching : any; import std.array : array; import std.exception : assumeUnique; import std.range : only; import std.utf : byCodeUnit; if (!segment.length) return only(S.fromTrustedString(null)); char sep = '\0'; if (segment[$-1] == '/') { sep = '/'; segment = segment[0 .. $-1]; } if (!segment.byCodeUnit.any!(c => c == '%')) return only(S(segment, sep)); string n = decodeSingleSegment(segment).array; return only(S(n, sep)); } deprecated unittest { struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} assert(decodeSegment!Segment("foo").equal([Segment("foo")])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo o\\")])); assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")])); } static auto decodeSingleSegment(string segment) @nogc { import std.string : indexOf; static int hexDigit(char ch) @safe nothrow @nogc { assert(ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f'); if (ch >= '0' && ch <= '9') return ch - '0'; else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; else return ch - 'A' + 10; } static struct R { @safe pure nothrow @nogc: private { string m_str; size_t m_index; } this(string s) { m_str = s; } @property bool empty() const { return m_index >= m_str.length; } @property R save() const { return this; } @property char front() const { auto ch = m_str[m_index]; if (ch != '%') return ch; auto a = m_str[m_index+1]; auto b = m_str[m_index+2]; return cast(char)(16 * hexDigit(a) + hexDigit(b)); } @property void popFront() { assert(!empty); if (m_str[m_index] == '%') m_index += 3; else m_index++; } } return R(segment); } unittest { scope (failure) assert(false); assert(decodeSingleSegment("foo").equal("foo")); assert(decodeSingleSegment("fo%20o\\").equal("fo o\\")); assert(decodeSingleSegment("foo%20").equal("foo ")); } static string encodeSegment(string segment) { import std.array : appender; foreach (i, char c; segment) { if (isAsciiAlphaNum(c)) continue; switch (c) { default: auto ret = appender!string; ret.put(segment[0 .. i]); encodeSegment(ret, segment[i .. $]); return ret.data; case '-', '.', '_', '~': case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': case ':', '@': break; } } return segment; } unittest { assert(encodeSegment("foo") == "foo"); assert(encodeSegment("foo bar") == "foo%20bar"); } static void encodeSegment(R)(ref R dst, string segment) { static immutable char[16] digit = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; foreach (char c; segment) { switch (c) { default: dst.put('%'); dst.put(digit[uint(c) / 16]); dst.put(digit[uint(c) % 16]); break; case 'a': .. case 'z': case 'A': .. case 'Z': case '0': .. case '9': case '-', '.', '_', '~': case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': case ':', '@': dst.put(c); break; } } } } private auto extension(R)(R filename) if (isForwardRange!R && isSomeChar!(ElementType!R)) { if (filename.empty) return filename; static if (isArray!R) { // avoid auto decoding filename = filename[1 .. $]; // ignore leading dot R candidate; while (filename.length) { if (filename[0] == '.') candidate = filename; filename = filename[1 .. $]; } return candidate; } else { filename.popFront(); // ignore leading dot R candidate; while (!filename.empty) { if (filename.front == '.') candidate = filename.save; filename.popFront(); } return candidate; } } @safe nothrow unittest { assert(extension("foo") == ""); assert(extension("foo.txt") == ".txt"); assert(extension(".foo") == ""); assert(extension(".foo.txt") == ".txt"); assert(extension("foo.bar.txt") == ".txt"); } unittest { assert(extension(InetPath("foo").head2.name).equal("")); assert(extension(InetPath("foo.txt").head2.name).equal(".txt")); assert(extension(InetPath(".foo").head2.name).equal("")); assert(extension(InetPath(".foo.txt").head2.name).equal(".txt")); assert(extension(InetPath("foo.bar.txt").head2.name).equal(".txt")); } private auto stripExtension(R)(R filename) if (isForwardRange!R && isSomeChar!(ElementType!R)) { static if (isArray!R) { // make sure to return a slice if (!filename.length) return filename; R r = filename; r = r[1 .. $]; // ignore leading dot size_t cnt = 0, rcnt = r.length; while (r.length) { if (r[0] == '.') rcnt = cnt; cnt++; r = r[1 .. $]; } return filename[0 .. rcnt + 1]; } else { if (filename.empty) return filename.takeExactly(0); R r = filename.save; size_t cnt = 0, rcnt = size_t.max; r.popFront(); // ignore leading dot while (!r.empty) { if (r.front == '.') rcnt = cnt; cnt++; r.popFront(); } if (rcnt == size_t.max) return filename.takeExactly(cnt + 1); return filename.takeExactly(rcnt + 1); } } @safe nothrow unittest { assert(stripExtension("foo") == "foo"); assert(stripExtension("foo.txt") == "foo"); assert(stripExtension(".foo") == ".foo"); assert(stripExtension(".foo.txt") == ".foo"); assert(stripExtension("foo.bar.txt") == "foo.bar"); } unittest { // test range based path import std.utf : byWchar; assert(stripExtension("foo".byWchar).equal("foo")); assert(stripExtension("foo.txt".byWchar).equal("foo")); assert(stripExtension(".foo".byWchar).equal(".foo")); assert(stripExtension(".foo.txt".byWchar).equal(".foo")); assert(stripExtension("foo.bar.txt".byWchar).equal("foo.bar")); assert(stripExtension(InetPath("foo").head2.name).equal("foo")); assert(stripExtension(InetPath("foo.txt").head2.name).equal("foo")); assert(stripExtension(InetPath(".foo").head2.name).equal(".foo")); assert(stripExtension(InetPath(".foo.txt").head2.name).equal(".foo")); assert(stripExtension(InetPath("foo.bar.txt").head2.name).equal("foo.bar")); } private static bool isAsciiAlphaNum(char ch) @safe nothrow pure @nogc { return (uint(ch) & 0xDF) - 0x41 < 26 || uint(ch) - '0' <= 9; } unittest { assert(!isAsciiAlphaNum('@')); assert(isAsciiAlphaNum('A')); assert(isAsciiAlphaNum('Z')); assert(!isAsciiAlphaNum('[')); assert(!isAsciiAlphaNum('`')); assert(isAsciiAlphaNum('a')); assert(isAsciiAlphaNum('z')); assert(!isAsciiAlphaNum('{')); assert(!isAsciiAlphaNum('/')); assert(isAsciiAlphaNum('0')); assert(isAsciiAlphaNum('9')); assert(!isAsciiAlphaNum(':')); } unittest { // regression tests assert(NativePath("").bySegment.empty); assert(NativePath("").bySegment2.empty); }