Properly implement struct Path.

The new implementation is @nogc where possible.
This commit is contained in:
Sönke Ludwig 2017-01-17 23:50:33 +01:00
parent 41d3c1411d
commit fb706ea0b6
No known key found for this signature in database
GPG key ID: D95E8DB493EE314C
2 changed files with 501 additions and 115 deletions

View file

@ -24,4 +24,4 @@ UDP connections | done
File I/O | done
Directory watchers | done
ManualEvent | done
Path/URL types | WIP
Path/URL types | done

View file

@ -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;