From fb706ea0b6d1a48c84dc340b97ee7d2cd26f05d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Tue, 17 Jan 2017 23:50:33 +0100 Subject: [PATCH] Properly implement struct Path. The new implementation is @nogc where possible. --- README.md | 2 +- source/vibe/core/path.d | 614 ++++++++++++++++++++++++++++++++-------- 2 files changed, 501 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 08fc0e3..631450e 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ UDP connections | done File I/O | done Directory watchers | done ManualEvent | done -Path/URL types | WIP +Path/URL types | done diff --git a/source/vibe/core/path.d b/source/vibe/core/path.d index 560cf3b..12d4f49 100644 --- a/source/vibe/core/path.d +++ b/source/vibe/core/path.d @@ -1,170 +1,556 @@ module vibe.core.path; +import std.algorithm.searching : commonPrefix, endsWith, startsWith; +import std.algorithm.comparison : min; +import std.algorithm.iteration : map; import std.exception : enforce; +import std.range : popFrontExactly, takeExactly; +import std.range.primitives : ElementType, isInputRange; -static import std.path; +/** 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 base_path) +@safe { + import std.array : replicate; + import std.array : array; + + assert(path.absolute && base_path.absolute, "Both arguments to relativeTo must be absolute paths."); + if (path.type == PathType.windows) { + // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case + if (path.absolute && !path.empty && + (path.front.toString().endsWith(":") && !base_path.startsWith(path[0 .. 1]) || + path.front == "\\" && !base_path.startsWith(path[0 .. min(2, $)]))) + { + return path; + } + } + + size_t base = commonPrefix(path[], base_path[]).length; + + auto ret = Path("../".replicate(base_path.length - base), path.type) ~ path[base .. $]; + if (path.endsWithSlash && !ret.endsWithSlash) ret ~= Path("./"); + return ret; +} + +/// +unittest { + with (PathType) { + import std.array : array; + import std.conv : to; + assert(Path("/some/path", posix).relativeTo(Path("/", posix)) == Path("some/path", posix)); + assert(Path("/some/path/", posix).relativeTo(Path("/some/other/path/", posix)) == Path("../../path/", posix)); + assert(Path("/some/path/", posix).relativeTo(Path("/some/other/path", posix)) == Path("../../path/", posix)); + + assert(Path("C:\\some\\path", windows).relativeTo(Path("C:\\", windows)) == Path("some\\path", windows)); + assert(Path("C:\\some\\path\\", windows).relativeTo(Path("C:\\some\\other\\path/", windows)) == Path("..\\..\\path\\", windows)); + assert(Path("C:\\some\\path\\", windows).relativeTo(Path("C:\\some\\other\\path", windows)) == Path("..\\..\\path\\", windows)); + + assert(Path("\\\\server\\some\\path", windows).relativeTo(Path("\\\\server\\", windows)) == Path("some\\path", windows)); + assert(Path("\\\\server\\some\\path\\", windows).relativeTo(Path("\\\\server\\some\\other\\path/", windows)) == Path("..\\..\\path\\", windows)); + assert(Path("\\\\server\\some\\path\\", windows).relativeTo(Path("\\\\server\\some\\other\\path", windows)) == Path("..\\..\\path\\", windows)); + + assert(Path("C:\\some\\path", windows).relativeTo(Path("D:\\", windows)) == Path("C:\\some\\path", windows)); + assert(Path("C:\\some\\path\\", windows).relativeTo(Path("\\\\server\\share", windows)) == Path("C:\\some\\path\\", windows)); + assert(Path("\\\\server\\some\\path\\", windows).relativeTo(Path("C:\\some\\other\\path", windows)) == Path("\\\\server\\some\\path\\", windows)); + } +} + + +/** 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 base_path) +@safe { + if (!base_path.endsWithSlash) { + if (base_path.length > 0) base_path = base_path.parentPath; + else base_path = Path("/", path.type); + } + return path.relativeTo(base_path); +} + +/// +unittest { + with (PathType) { + assert(Path("/some/path", inet).relativeToWeb(Path("/", inet)) == Path("some/path", inet)); + assert(Path("/some/path/", inet).relativeToWeb(Path("/some/other/path/", inet)) == Path("../../path/", inet)); + assert(Path("/some/path/", inet).relativeToWeb(Path("/some/other/path", inet)) == Path("../path/", inet)); + } +} struct Path { - @safe: - private string m_path; - - this(string p) - nothrow { - m_path = p; +@safe: + private { + string m_path; + size_t m_length; + size_t m_nextEntry = size_t.max; + PathType m_type; + bool m_absolute; } - @property bool absolute() + this(string p, PathType type = PathType.native) + nothrow @nogc { - import std.algorithm.searching : startsWith; - // FIXME: Windows paths! - return m_path.startsWith('/'); + import std.range.primitives : walkLength; + + m_path = p; + m_type = type; + setupPath(); } + @property bool absolute() const nothrow @nogc { return m_absolute; } + + @property bool empty() const nothrow @nogc { return m_path.length == 0; } + + @property size_t length() const nothrow @nogc { return m_length; } + + @property PathType type() const nothrow @nogc { return m_type; } + + @property PathEntry front() const nothrow @nogc { return PathEntry(m_path[0 .. m_nextEntry], m_type, m_absolute); } + + @property Path save() const nothrow @nogc { return this; } + @property bool endsWithSlash() - const { - import std.algorithm.searching : endsWith; - return m_path.endsWith('/'); + const nothrow @nogc { + import std.algorithm.comparison : among; + + final switch (m_type) { + case PathType.posix, PathType.inet: + return m_path.length > 0 && m_path[$-1] == '/'; + case PathType.windows: + return m_path.length > 0 && m_path[$-1].among('/', '\\'); + } } + @property void endsWithSlash(bool v) - { - import std.algorithm.searching : endsWith; + nothrow { + bool ews = this.endsWithSlash; - if (!m_path.endsWith('/') && v) m_path ~= '/'; - else if (m_path.endsWith('/') && !v) m_path = m_path[0 .. $-1]; // FIXME: "/test//" -> "/test/" + final switch (m_type) { + case PathType.posix, PathType.inet: + if (!ews && v) m_path ~= '/'; + else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME: "/test//" -> "/test/" + break; + case PathType.windows: + if (!ews && v) m_path ~= '\\'; + else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME: "/test//" -> "/test/" + break; + } } - @property bool empty() const { return m_path.length == 0 || m_path == "/"; } + void popFront() + nothrow @nogc { + import std.string : indexOf; + m_path = m_path[min(m_nextEntry, $) .. $]; + m_absolute = false; + + final switch (m_type) { + case PathType.posix, PathType.inet: + auto idx = m_path.indexOf('/'); + m_nextEntry = idx >= 0 ? idx+1 : m_path.length; + break; + case PathType.windows: + auto idx = m_path.indexOf('\\'); + auto idx2 = m_path[0 .. idx >= 0 ? idx : $].indexOf('/'); + m_nextEntry = idx2 >= 0 ? idx2+1 : idx >= 0 ? idx+1 : m_path.length; + break; + } + } Path parentPath() - { + @nogc { import std.string : lastIndexOf; auto idx = m_path.lastIndexOf('/'); version (Windows) { auto idx2 = m_path.lastIndexOf('\\'); if (idx2 > idx) idx = idx2; } - enforce(idx > 0, "Path has no parent path."); + // FIXME: handle Windows root path cases + static const Exception e = new Exception("Path has no parent path"); + if (idx <= 0) throw e; return Path(m_path[0 .. idx+1]); } - Path relativeTo(Path p) { assert(false, "TODO!"); } - Path relativeToWeb(Path p) { assert(false, "TODO!"); } - void normalize() { - //assert(false, "TODO!"); - } + import std.array : join; - // FIXME: this should decompose the two paths into their parts and compare the part sequence - bool startsWith(Path other) const { - import std.algorithm.searching : startsWith; - return m_path[].startsWith(other.m_path); - } + auto ews = this.endsWithSlash; - string toString() const nothrow { return m_path; } - - string toNativeString() const nothrow { return m_path; } - - PathEntry opIndex(size_t idx) - const { - import std.string : indexOf; - - string s = m_path; - while (true) { - auto sidx = m_path.indexOf('/'); - if (idx == 0) break; - enforce(sidx > 0, "Index out of bounds"); - s = s[sidx+1 .. $]; - idx--; + PathEntry[] newnodes; + foreach (n; this[]) { + switch(n.toString()){ + default: newnodes ~= n; break; + case "", ".": break; + case "..": + enforce(!this.absolute || newnodes.length > 0, "Path goes below root node."); + if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; + else newnodes ~= n; + break; + } } - auto sidx = s.indexOf('/'); - return PathEntry(s[0 .. sidx >= 0 ? sidx : $]); + final switch (m_type) { + case PathType.posix, PathType.inet: + m_path = newnodes.map!(n => n.toString()).join('/'); + if (m_absolute) m_path = '/' ~ m_path; + if (ews) m_path ~= '/'; + setupPath(); + break; + case PathType.windows: + m_path = newnodes.map!(n => n.toString()).join('\\'); + if (ews) m_path ~= '\\'; + setupPath(); + break; + } } - Path opBinary(string op : "~")(string subpath) { return this ~ Path(subpath); } - Path opBinary(string op : "~")(Path subpath) { return Path(std.path.buildPath(m_path, subpath.toString())); } + string toString() const nothrow @nogc { return m_path; } + + string toString(PathType type) const nothrow { + import std.array : join; + + if (m_type == type) return m_path; + if (type == PathType.windows) { + return this[].map!(p => p.toString()).join('\\'); + } else { + if (m_type == PathType.windows) { + if (m_absolute) return '/' ~ this[].map!(n => n.toString()).join('/'); + else return this[].map!(n => n.toString()).join('/'); + } else return m_path; + } + } + + string toNativeString() const nothrow { return toString(PathType.native); } + + Path opSlice() const nothrow @nogc { return this; } + + Path opSlice(size_t from, size_t to) + const nothrow @nogc { + Path ret = this; + foreach (i; 0 .. from) ret.popFront(); + auto rs = ret.toString(); + foreach (i; from .. to) ret.popFront(); + return Path(rs[0 .. $-ret.toString().length], m_type); + } + + size_t opDollar() const nothrow @nogc { return m_length; } + + PathEntry opIndex(size_t idx) const nothrow @nogc { auto ret = this[]; ret.popFrontExactly(idx); return ret.front; } + + Path opBinary(string op : "~")(string subpath) nothrow { return this ~ Path(subpath); } + Path opBinary(string op : "~")(Path subpath) nothrow { + assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path."); + + if (this.endsWithSlash) + return Path(m_path ~ subpath.m_path, m_type); + if (!m_path.length) return subpath; + + final switch (m_type) { + case PathType.inet, PathType.posix: + return Path(m_path ~ '/' ~ subpath.m_path, m_type); + case PathType.windows: + return Path(m_path ~ '\\' ~ subpath.m_path, m_type); + } + } + + Path opBinary(string op : "~", R)(R entries) nothrow + if (!is(R == Path) && isInputRange!R && is(ElementType!R == PathEntry)) + { + import std.array : join; + + final switch (m_type) { + case PathType.inet, PathType.posix: + auto rpath = entries.map!(e => e.toString()).join('/'); + if (this.empty) return Path(rpath, m_type); + if (this.endsWithSlash) return Path(m_path ~ rpath, m_type); + return Path(m_path ~ '/' ~ rpath, m_type); + case PathType.windows: + auto rpath = entries.map!(e => e.toString()).join('\\'); + if (this.empty) return Path(rpath, m_type); + if (this.endsWithSlash) return Path(m_path ~ rpath, m_type); + return Path(m_path ~ '\\' ~ rpath, m_type); + } + } + + void opOpAssign(string op : "~", T)(T op) { this = this ~ op; } + + bool opEquals(Path other) const @nogc { import std.algorithm.comparison : equal; return this[].equal(other); } + + private void setupPath() + nothrow @nogc { + auto ap = getAbsolutePrefix(m_path, type); + if (ap.length) { + m_nextEntry = ap.length; + m_absolute = true; + } else { + m_nextEntry = 0; + popFront(); + } + + auto pr = this; + while (!pr.empty) { + m_length++; + pr.popFront(); + } + } } -struct PathEntry { - nothrow: @safe: - private string m_name; +unittest { + import std.algorithm.comparison : equal; + + with (PathType) { + assert(Path("hello/world", posix).equal([PathEntry("hello/", posix), PathEntry("world", posix)])); + assert(Path("/hello/world/", posix).equal([PathEntry("/", posix), PathEntry("hello/", posix), PathEntry("world/", posix)])); + assert(Path("hello\\world", posix).equal([PathEntry("hello\\world", posix)])); + assert(Path("hello/world", windows).equal([PathEntry("hello/", windows), PathEntry("world", windows)])); + assert(Path("/hello/world/", windows).equal([PathEntry("/", windows), PathEntry("hello/", windows), PathEntry("world/", windows)])); + assert(Path("hello\\w/orld", windows).equal([PathEntry("hello\\", windows), PathEntry("w/", windows), PathEntry("orld", windows)])); + assert(Path("hello/w\\orld", windows).equal([PathEntry("hello/", windows), PathEntry("w\\", windows), PathEntry("orld", windows)])); + } +} + +unittest +{ + import std.algorithm.comparison : equal; - this(string name) { - m_name = name; + auto unc = "\\\\server\\share\\path"; + auto uncp = Path(unc, PathType.windows); + assert(uncp.absolute); + uncp.normalize(); + version(Windows) assert(uncp.toNativeString() == unc); + assert(uncp.absolute); + assert(!uncp.endsWithSlash); + } + + { + auto abspath = "/test/path/"; + auto abspathp = Path(abspath, PathType.posix); + assert(abspathp.toString() == abspath); + version(Windows) {} else assert(abspathp.toNativeString() == abspath); + assert(abspathp.absolute); + assert(abspathp.endsWithSlash); + assert(abspathp.length == 3); + assert(abspathp[0] == ""); + assert(abspathp[1] == "test"); + assert(abspathp[2] == "path"); + } + + { + auto relpath = "test/path/"; + auto relpathp = Path(relpath, PathType.posix); + assert(relpathp.toString() == relpath); + version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); + else assert(relpathp.toNativeString() == relpath); + assert(!relpathp.absolute); + assert(relpathp.endsWithSlash); + assert(relpathp.length == 2); + assert(relpathp[0] == "test"); + assert(relpathp[1] == "path"); + } + + { + auto winpath = "C:\\windows\\test"; + auto winpathp = Path(winpath, PathType.windows); + assert(winpathp.toString() == "C:\\windows\\test"); + assert(winpathp.toString(PathType.posix) == "/C:/windows/test"); + version(Windows) assert(winpathp.toNativeString() == winpath); + else assert(winpathp.toNativeString() == "/C:/windows/test"); + assert(winpathp.absolute); + assert(!winpathp.endsWithSlash); + assert(winpathp.length == 3); + assert(winpathp[0] == "C:"); + assert(winpathp[1] == "windows"); + assert(winpathp[2] == "test"); + } + + { + auto dotpath = "/test/../test2/././x/y"; + auto dotpathp = Path(dotpath, PathType.posix); + 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 = Path(dotpath, PathType.posix); + assert(dotpathp.toString() == "/test/..////test2//./x/y"); + dotpathp.normalize(); + assert(dotpathp.toString() == "/test2/x/y"); + } + + { + auto parentpath = "/path/to/parent"; + auto parentpathp = Path(parentpath, PathType.posix); + auto subpath = "/path/to/parent/sub/"; + auto subpathp = Path(subpath, PathType.posix); + auto subpath_rel = "sub/"; + assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); + auto subfile = "/path/to/parent/child"; + auto subfilep = Path(subfile, PathType.posix); + auto subfile_rel = "child"; + assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); + } + + { // relative paths across Windows devices are not allowed + auto p1 = Path("\\\\server\\share", PathType.windows); assert(p1.absolute); + auto p2 = Path("\\\\server\\othershare", PathType.windows); assert(p2.absolute); + auto p3 = Path("\\\\otherserver\\share", PathType.windows); assert(p3.absolute); + auto p4 = Path("C:\\somepath", PathType.windows); assert(p4.absolute); + auto p5 = Path("C:\\someotherpath", PathType.windows); assert(p5.absolute); + auto p6 = Path("D:\\somepath", PathType.windows); assert(p6.absolute); + assert(p4.relativeTo(p5) == Path("../somepath", PathType.windows)); + assert(p4.relativeTo(p6) == Path("C:\\somepath", PathType.windows)); + assert(p4.relativeTo(p1) == Path("C:\\somepath", PathType.windows)); + assert(p1.relativeTo(p2) == Path("../share", PathType.windows)); + assert(p1.relativeTo(p3) == Path("\\\\server\\share", PathType.windows)); + assert(p1.relativeTo(p4) == Path("\\\\server\\share", PathType.windows)); + } + + { // relative path, trailing slash + auto p1 = Path("/some/path", PathType.posix); + auto p2 = Path("/some/path/", PathType.posix); + assert(p1.relativeTo(p1).toString() == ""); + assert(p1.relativeTo(p2).toString() == ""); + assert(p2.relativeTo(p2).toString() == "./"); + } + + assert(Path("").empty); + assert(Path("a/b/c")[1 .. 3].map!(p => p.toString()).equal(["b", "c"])); + + assert(Path("/", PathType.posix) ~ Path("foo/bar") == Path("/foo/bar")); + assert(Path("", PathType.posix) ~ Path("foo/bar") == Path("foo/bar")); + assert(Path("foo", PathType.posix) ~ Path("bar") == Path("foo/bar")); + assert(Path("foo/", PathType.posix) ~ Path("bar") == Path("foo/bar")); +} + + +struct PathEntry { + import std.string : cmp; + + @safe pure nothrow: + + private { + string m_name; + bool m_hasSeparator; + } + + this(string str, PathType pt, bool is_absolute_prefix = false) + @nogc { + import std.algorithm.searching : any; + + m_name = str; + + if (m_name.length > 0) { + final switch (pt) { + case PathType.inet, PathType.posix: + m_hasSeparator = m_name[$-1] == '/'; + break; + case PathType.windows: + m_hasSeparator = m_name[$-1] == '/' || m_name[$-1] == '\\'; + break; + } + } + + debug if (!is_absolute_prefix) + foreach (char ch; str[0 .. $-min(1, $)]) { + assert(ch != '/', "Invalid path entry."); + if (pt == PathType.windows) + assert(ch != '\\', "Invalid Windows path entry."); + } } alias toString this; - string toString() { return m_name; } + string toString() const @nogc { return m_name[0 .. m_hasSeparator ? $-1 : $]; } + + string toFullString() const @nogc { return m_name; } + + Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path([this, rhs], false); } + + bool opEquals(ref const PathEntry rhs) const @nogc { return this == rhs.toString(); } + bool opEquals(PathEntry rhs) const @nogc { return this == rhs.toString(); } + bool opEquals(string rhs) const @nogc { return this.toString() == rhs; } + int opCmp(ref const PathEntry rhs) const @nogc { return this.toString().cmp(rhs.toString()); } + int opCmp(string rhs) const @nogc { return this.toString().cmp(rhs); } } -private enum PathType { - unix, +enum PathType { + posix, windows, - inet + inet, + native = isWindows ? windows : posix } -private struct PathEntryRange(PathType TYPE) { - enum type = TYPE; - private { - string m_path; - PathEntry m_front; - } +private string getAbsolutePrefix(string path, PathType type) +@safe nothrow @nogc { + import std.string : indexOfAny; + import std.algorithm.comparison : among; - this(string path) - { - m_path = path; - skipLeadingSeparator(); - } - - @property bool empty() const { return m_path.length == 0; } - - @property ref const(PathEntry) front() const { return m_front; } - - void popFront() - { - import std.string : indexOf; - - auto idx = m_path.indexOf('/'); - static if (type == PathType.windows) { - if (idx >= 0) { - auto idx2 = m_path[0 .. idx].indexOf('\\'); - if (idx2 >= 0) idx = idx2; - } else idx = m_path.indexOf('\\'); - } - - if (idx < 0) { - m_front = PathEntry(m_path); - m_path = null; - } else { - m_front = PathEntry(m_path[0 .. idx]); - m_path = m_path[idx+1 .. $]; - } - } - - private void skipLeadingSeparator() - { - import std.algorithm.searching : startsWith; - - if (m_path.startsWith('/')) m_path = m_path[1 .. $]; - else static if (type == PathType.windows) { - if (m_path.startsWith('\\')) m_path = m_path[1 .. $]; - } + final switch (type) { + case PathType.posix, PathType.inet: + if (path.length > 0 && path[0] == '/') + return path[0 .. 1]; + return null; + case PathType.windows: + if (path.length >= 2 && path[0 .. 2] == "\\\\") + return path[0 .. 2]; + foreach (i; 1 .. path.length) + if (path[i].among!('/', '\\')) { + if (path[i-1] == ':') + return path[0 .. i+1]; + break; + } + return null; } } -/*unittest { - import std.algorithm.comparison : equal; +unittest { + assert(getAbsolutePrefix("/", PathType.posix) == "/"); + assert(getAbsolutePrefix("/test", PathType.posix) == "/"); + assert(getAbsolutePrefix("/test/", PathType.posix) == "/"); + assert(getAbsolutePrefix("test/", PathType.posix) == ""); + assert(getAbsolutePrefix("", PathType.posix) == ""); + assert(getAbsolutePrefix("./", PathType.posix) == ""); - assert(PathEntryRange!(PathType.unix)("hello/world").equal([PathEntry("hello"), PathEntry("world")])); - assert(PathEntryRange!(PathType.unix)("/hello/world/").equal([PathEntry("hello"), PathEntry("world")])); - assert(PathEntryRange!(PathType.unix)("hello\\world").equal([PathEntry("hello\\world")])); - assert(PathEntryRange!(PathType.windows)("hello/world").equal([PathEntry("hello"), PathEntry("world")])); - assert(PathEntryRange!(PathType.windows)("/hello/world/").equal([PathEntry("hello"), PathEntry("world")])); - assert(PathEntryRange!(PathType.windows)("hello\\w/orld").equal([PathEntry("hello"), PathEntry("w"), PathEntry("orld")])); - assert(PathEntryRange!(PathType.windows)("hello/w\\orld").equal([PathEntry("hello"), PathEntry("w"), PathEntry("orld")])); -}*/ + assert(getAbsolutePrefix("/", PathType.inet) == "/"); + assert(getAbsolutePrefix("/test", PathType.inet) == "/"); + assert(getAbsolutePrefix("/test/", PathType.inet) == "/"); + assert(getAbsolutePrefix("test/", PathType.inet) == ""); + assert(getAbsolutePrefix("", PathType.inet) == ""); + assert(getAbsolutePrefix("./", PathType.inet) == ""); + + assert(getAbsolutePrefix("/test", PathType.windows) == ""); + assert(getAbsolutePrefix("\\test", PathType.windows) == ""); + assert(getAbsolutePrefix("C:\\", PathType.windows) == "C:\\"); + assert(getAbsolutePrefix("C:\\test", PathType.windows) == "C:\\"); + assert(getAbsolutePrefix("C:\\test\\", PathType.windows) == "C:\\"); + assert(getAbsolutePrefix("C:/", PathType.windows) == "C:/"); + assert(getAbsolutePrefix("C:/test", PathType.windows) == "C:/"); + assert(getAbsolutePrefix("C:/test/", PathType.windows) == "C:/"); + assert(getAbsolutePrefix("\\\\server", PathType.windows) == "\\\\"); + assert(getAbsolutePrefix("\\\\server\\", PathType.windows) == "\\\\"); + assert(getAbsolutePrefix("\\\\.\\", PathType.windows) == "\\\\"); + assert(getAbsolutePrefix("\\\\?\\", PathType.windows) == "\\\\"); +} + +version (Windows) private enum isWindows = true; +else private enum isWindows = false;