diff --git a/source/vibe/core/file.d b/source/vibe/core/file.d index cd96b0b..2e98dc5 100644 --- a/source/vibe/core/file.d +++ b/source/vibe/core/file.d @@ -35,7 +35,7 @@ version(Posix){ /** Opens a file stream with the specified mode. */ -FileStream openFile(Path path, FileMode mode = FileMode.read) +FileStream openFile(NativePath path, FileMode mode = FileMode.read) { auto fil = eventDriver.files.open(path.toNativeString(), cast(FileOpenMode)mode); enforce(fil != FileFD.invalid, "Failed to open file '"~path.toNativeString~"'"); @@ -44,7 +44,7 @@ FileStream openFile(Path path, FileMode mode = FileMode.read) /// ditto FileStream openFile(string path, FileMode mode = FileMode.read) { - return openFile(Path(path), mode); + return openFile(NativePath(path), mode); } @@ -58,7 +58,7 @@ FileStream openFile(string path, FileMode mode = FileMode.read) path = The path of the file to read buffer = An optional buffer to use for storing the file contents */ -ubyte[] readFile(Path path, ubyte[] buffer = null, size_t max_size = size_t.max) +ubyte[] readFile(NativePath path, ubyte[] buffer = null, size_t max_size = size_t.max) { auto fil = openFile(path); scope (exit) fil.close(); @@ -71,14 +71,14 @@ ubyte[] readFile(Path path, ubyte[] buffer = null, size_t max_size = size_t.max) /// ditto ubyte[] readFile(string path, ubyte[] buffer = null, size_t max_size = size_t.max) { - return readFile(Path(path), buffer, max_size); + return readFile(NativePath(path), buffer, max_size); } /** Write a whole file at once. */ -void writeFile(Path path, in ubyte[] contents) +void writeFile(NativePath path, in ubyte[] contents) { auto fil = openFile(path, FileMode.createTrunc); scope (exit) fil.close(); @@ -87,13 +87,13 @@ void writeFile(Path path, in ubyte[] contents) /// ditto void writeFile(string path, in ubyte[] contents) { - writeFile(Path(path), contents); + writeFile(NativePath(path), contents); } /** Convenience function to append to a file. */ -void appendToFile(Path path, string data) { +void appendToFile(NativePath path, string data) { auto fil = openFile(path, FileMode.append); scope(exit) fil.close(); fil.write(data); @@ -101,7 +101,7 @@ void appendToFile(Path path, string data) { /// ditto void appendToFile(string path, string data) { - appendToFile(Path(path), data); + appendToFile(NativePath(path), data); } /** @@ -110,7 +110,7 @@ void appendToFile(string path, string data) The resulting string will be sanitized and will have the optional byte order mark (BOM) removed. */ -string readFileUTF8(Path path) +string readFileUTF8(NativePath path) { import vibe.internal.string; @@ -119,7 +119,7 @@ string readFileUTF8(Path path) /// ditto string readFileUTF8(string path) { - return readFileUTF8(Path(path)); + return readFileUTF8(NativePath(path)); } @@ -128,7 +128,7 @@ string readFileUTF8(string path) The file will have a byte order mark (BOM) prepended. */ -void writeFileUTF8(Path path, string contents) +void writeFileUTF8(NativePath path, string contents) { static immutable ubyte[] bom = [0xEF, 0xBB, 0xBF]; auto fil = openFile(path, FileMode.createTrunc); @@ -163,7 +163,7 @@ FileStream createTempFile(string suffix = null) auto fd = () @trusted { return mkstemps(templ.ptr, cast(int)suffix.length); } (); enforce(fd >= 0, "Failed to create temporary file."); auto efd = eventDriver.files.adopt(fd); - return FileStream(efd, Path(templ[0 .. $-1].idup), FileMode.createTrunc); + return FileStream(efd, NativePath(templ[0 .. $-1].idup), FileMode.createTrunc); } } @@ -176,7 +176,7 @@ FileStream createTempFile(string suffix = null) copy_fallback = Determines if copy/remove should be used in case of the source and destination path pointing to different devices. */ -void moveFile(Path from, Path to, bool copy_fallback = false) +void moveFile(NativePath from, NativePath to, bool copy_fallback = false) { moveFile(from.toNativeString(), to.toNativeString(), copy_fallback); } @@ -210,7 +210,7 @@ void moveFile(string from, string to, bool copy_fallback = false) Throws: An Exception if the copy operation fails for some reason. */ -void copyFile(Path from, Path to, bool overwrite = false) +void copyFile(NativePath from, NativePath to, bool overwrite = false) { { auto src = openFile(from, FileMode.read); @@ -226,13 +226,13 @@ void copyFile(Path from, Path to, bool overwrite = false) /// ditto void copyFile(string from, string to) { - copyFile(Path(from), Path(to)); + copyFile(NativePath(from), NativePath(to)); } /** Removes a file */ -void removeFile(Path path) +void removeFile(NativePath path) { removeFile(path.toNativeString()); } @@ -245,7 +245,7 @@ void removeFile(string path) /** Checks if a file exists */ -bool existsFile(Path path) nothrow +bool existsFile(NativePath path) nothrow { return existsFile(path.toNativeString()); } @@ -262,7 +262,7 @@ bool existsFile(string path) nothrow Throws: A `FileException` is thrown if the file does not exist. */ -FileInfo getFileInfo(Path path) +FileInfo getFileInfo(NativePath path) @trusted { auto ent = DirEntry(path.toNativeString()); return makeFileInfo(ent); @@ -270,26 +270,26 @@ FileInfo getFileInfo(Path path) /// ditto FileInfo getFileInfo(string path) { - return getFileInfo(Path(path)); + return getFileInfo(NativePath(path)); } /** Creates a new directory. */ -void createDirectory(Path path) +void createDirectory(NativePath path) { () @trusted { mkdir(path.toNativeString()); } (); } /// ditto void createDirectory(string path) { - createDirectory(Path(path)); + createDirectory(NativePath(path)); } /** Enumerates all files in the specified directory. */ -void listDirectory(Path path, scope bool delegate(FileInfo info) del) +void listDirectory(NativePath path, scope bool delegate(FileInfo info) del) @trusted { foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) if( !del(makeFileInfo(ent)) ) @@ -298,10 +298,10 @@ void listDirectory(Path path, scope bool delegate(FileInfo info) del) /// ditto void listDirectory(string path, scope bool delegate(FileInfo info) del) { - listDirectory(Path(path), del); + listDirectory(NativePath(path), del); } /// ditto -int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) +int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path) { int iterator(scope int delegate(ref FileInfo) del){ int ret = 0; @@ -316,28 +316,28 @@ int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) /// ditto int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) { - return iterateDirectory(Path(path)); + return iterateDirectory(NativePath(path)); } /** Starts watching a directory for changes. */ -DirectoryWatcher watchDirectory(Path path, bool recursive = true) +DirectoryWatcher watchDirectory(NativePath path, bool recursive = true) { return DirectoryWatcher(path, recursive); } // ditto DirectoryWatcher watchDirectory(string path, bool recursive = true) { - return watchDirectory(Path(path), recursive); + return watchDirectory(NativePath(path), recursive); } /** Returns the current working directory. */ -Path getWorkingDirectory() +NativePath getWorkingDirectory() { - return Path(() @trusted { return std.file.getcwd(); } ()); + return NativePath(() @trusted { return std.file.getcwd(); } ()); } @@ -384,7 +384,7 @@ struct FileStream { @safe: private struct CTX { - Path path; + NativePath path; ulong size; FileMode mode; ulong ptr; @@ -395,7 +395,7 @@ struct FileStream { CTX* m_ctx; } - private this(FileFD fd, Path path, FileMode mode) + private this(FileFD fd, NativePath path, FileMode mode) { assert(fd != FileFD.invalid, "Constructing FileStream from invalid file descriptor."); m_fd = fd; @@ -420,7 +420,7 @@ struct FileStream { @property int fd() { return cast(int)m_fd; } /// The path of the file. - @property Path path() const { return ctx.path; } + @property NativePath path() const { return ctx.path; } /// Determines if the file stream is still open @property bool isOpen() const { return m_fd != FileFD.invalid; } @@ -568,7 +568,7 @@ struct DirectoryWatcher { // TODO: avoid all those heap allocations! @safe: private static struct Context { - Path path; + NativePath path; bool recursive; Appender!(DirectoryChange[]) changes; LocalManualEvent changeEvent; @@ -581,7 +581,7 @@ struct DirectoryWatcher { // TODO: avoid all those heap allocations! case FileChangeKind.removed: ct = DirectoryChangeType.removed; break; case FileChangeKind.modified: ct = DirectoryChangeType.modified; break; } - this.changes ~= DirectoryChange(ct, Path(change.directory) ~ change.name.idup); + this.changes ~= DirectoryChange(ct, NativePath.fromTrustedString(change.directory) ~ NativePath.fromTrustedString(change.name.idup)); this.changeEvent.emit(); } } @@ -591,7 +591,7 @@ struct DirectoryWatcher { // TODO: avoid all those heap allocations! Context* m_context; } - private this(Path path, bool recursive) + private this(NativePath path, bool recursive) { m_context = new Context; // FIME: avoid GC allocation (use FD user data slot) m_watcher = eventDriver.watchers.watchDirectory(path.toNativeString, recursive, &m_context.onChange); @@ -604,7 +604,7 @@ struct DirectoryWatcher { // TODO: avoid all those heap allocations! ~this() nothrow { if (m_watcher != WatcherID.invalid) eventDriver.watchers.releaseRef(m_watcher); } /// The path of the watched directory - @property Path path() const nothrow { return m_context.path; } + @property NativePath path() const nothrow { return m_context.path; } /// Indicates if the directory is watched recursively @property bool recursive() const nothrow { return m_context.recursive; } @@ -665,7 +665,7 @@ struct DirectoryChange { DirectoryChangeType type; /// Path of the file/directory that was changed - Path path; + NativePath path; } diff --git a/source/vibe/core/path.d b/source/vibe/core/path.d index 680af36..58621e4 100644 --- a/source/vibe/core/path.d +++ b/source/vibe/core/path.d @@ -1,11 +1,19 @@ +/** + Contains routines for high level path handling. + + Copyright: © 2012-2017 RejectedSoftware e.K. + 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 : min; import std.algorithm.iteration : map; import std.exception : enforce; -import std.range : popFrontExactly, takeExactly; -import std.range.primitives : ElementType, isInputRange; +import std.range : empty, front, popFront, popFrontExactly, takeExactly; +import std.range.primitives : ElementType, isInputRange, isOutputRange; +import std.traits : isInstanceOf; /** Computes the relative path from `base_path` to this path. @@ -16,50 +24,122 @@ import std.range.primitives : ElementType, isInputRange; See_also: `relativeToWeb` */ -Path relativeTo(Path path, Path base_path) -@safe { - import std.array : replicate; - import std.array : array; +Path relativeTo(Path)(Path path, Path base_path) @safe + if (isInstanceOf!(GenericPath, Path)) +{ + import std.algorithm.comparison : equal; + 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 (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, $)]))) + + if (is(Path == WindowsPath)) { // FIXME: this shouldn't be a special case here! + bool samePrefix(size_t n) { - return path; + return path.bySegment.map!(n => n.name).take(n).equal(base_path.bySegment.map!(n => n.name).take(n)); + } + // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case + auto pref = path.bySegment; + if (!pref.empty && pref.front.name == "") { + pref.popFront(); + if (!pref.empty) { + // different drive? + if (pref.front.name.endsWith(':') && !samePrefix(2)) + return path; + // different UNC path? + if (pref.front.name == "" && !samePrefix(4)) + return path; + } } } - size_t base = commonPrefix(path[], base_path[]).length; + auto nodes = path.bySegment; + auto base_nodes = base_path.bySegment; - auto ret = Path("../".replicate(base_path.length - base), path.type) ~ path[base .. $]; - if (path.endsWithSlash && !ret.endsWithSlash) ret ~= Path("./", path.type); + // skip and count common prefix + size_t base = 0; + while (!nodes.empty && !base_nodes.empty && nodes.front.name == base_nodes.front.name) { + nodes.popFront(); + base_nodes.popFront(); + base++; + } + + enum up = Path.Segment("..", Path.defaultSeparator); + auto ret = Path(base_nodes.map!(p => up).chain(nodes)); + if (path.endsWithSlash) { + if (ret.empty) return Path("." ~ path.toString()[$-1]); + else ret.endsWithSlash = true; + } 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)); + 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(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(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(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(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(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)); + 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() == "./"); } } @@ -76,275 +156,464 @@ unittest { See_also: `relativeTo` */ -Path relativeToWeb(Path path, Path base_path) -@safe { +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.length > 0) base_path = base_path.parentPath; - else base_path = Path("/", path.type); + if (base_path.hasParentPath) base_path = base_path.parentPath; + else base_path = Path("/"); assert(base_path.absolute); } 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)); - } +/+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(); } -struct Path { + +/// 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: - private { - string m_path; - size_t m_length; - size_t m_nextEntry = size_t.max; - PathType m_type; - bool m_absolute; + alias Format = F; + + /** A single path segment. + */ + static struct Segment { + @safe: + + private { + string m_name; + 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_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; } } - this(string p, PathType type = PathType.native) - nothrow @nogc - { - import std.range.primitives : walkLength; + /** Represents a path as an forward range of `Segment`s. + */ + static struct PathRange { + import std.traits : ReturnType; - m_path = p; - m_type = type; - setupPath(); - } + private { + string m_path; + ReturnType!(Format.decodeSegment!Segment) m_fronts; + } - @property bool absolute() const nothrow @nogc { return m_absolute; } + private this(string path) + { + m_path = path; + if (m_path.length) { + auto ap = Format.getAbsolutePrefix(m_path); + if (ap.length) { + m_fronts = Format.decodeSegment!Segment(ap); + m_path = m_path[ap.length .. $]; + assert(!m_fronts.empty); + } else readFront(); + } + } - @property bool empty() const nothrow @nogc { return m_path.length == 0; } + @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_fronts.empty; } - @property size_t length() const nothrow @nogc { return m_length; } + @property PathRange save() { return this; } - @property PathType type() const nothrow @nogc { return m_type; } + @property Segment front() { return m_fronts.front; } - @property PathEntry front() const nothrow @nogc { return PathEntry(m_path[0 .. m_nextEntry], m_type, m_absolute); } + void popFront() + nothrow { + assert(!m_fronts.empty); + m_fronts.popFront(); + if (m_fronts.empty && m_path.length) + readFront(); + } - @property PathEntry head() const nothrow @nogc { return this[length-1]; } - - @property Path save() const nothrow @nogc { return this; } - - @property bool endsWithSlash() - 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('/', '\\'); + private void readFront() + { + auto n = Format.getFrontNode(m_path); + m_fronts = Format.decodeSegment!Segment(n); + m_path = m_path[n.length .. $]; + assert(!m_fronts.empty); } } + 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)); + } + + /** 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; + } + + /** 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; - - 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; - } + if (!ews && v) m_path ~= Format.defaultSeparator; + else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME?: "/test//" -> "/test/" } - void popFront() - nothrow @nogc { - import std.string : indexOf; + /// Iterates over the path by `Segment`. + @property PathRange bySegment() const { return PathRange(m_path); } - 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; + /// Returns the trailing segment of the path. + @property Segment head() + const { + auto s = Format.decodeSegment!Segment(Format.getBackNode(m_path)); + auto ret = s.front; + while (!s.empty) { + s.popFront(); + if (!s.empty) ret = s.front; } + return ret; } - Path parentPath() - @nogc { - import std.string : lastIndexOf; - auto idx = m_path.lastIndexOf('/'); - if (m_type == PathType.windows) { - auto idx2 = m_path.lastIndexOf('\\'); - if (idx2 > idx) idx = idx2; - } - // FIXME: handle Windows root path cases + /** Determines if the `parentPath` property is valid. + */ + 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. + */ + GenericPath parentPath() + const @nogc { + auto b = Format.getBackNode(m_path); static const Exception e = new Exception("Path has no parent path"); - if (idx <= 0) throw e; - return Path(m_path[0 .. idx+1], m_type); + if (b.length >= m_path.length) throw e; + return GenericPath.fromTrustedString(m_path[0 .. b.length]); } + /** 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 : join; + import std.array : appender, join; - auto ews = this.endsWithSlash; - - PathEntry[] newnodes; - foreach (n; this[]) { - switch(n.toString()){ + Segment[] newnodes; + bool got_non_sep = false; + foreach (n; this.bySegment) { + if (n.hasSeparator) n.separator = Format.defaultSeparator; + if (!got_non_sep) { + if (n.name == "") newnodes ~= n; + else got_non_sep = true; + } + switch (n.name) { 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]; + if (newnodes.length > 0 && newnodes[$-1].name != "..") newnodes = newnodes[0 .. $-1]; else newnodes ~= n; break; } } - 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; - } + 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; } - string toString(PathType type) const nothrow { - import std.array : join; - - if (m_type == type) return m_path; - if (type == PathType.windows) { - auto ret = (absolute ? this[1 .. $] : this[]).map!(p => p.toString()).join('\\'); - if (endsWithSlash) ret ~= '\\'; - return ret; - } else { - if (m_type == PathType.windows) { - string ret; - if (m_absolute) ret = '/' ~ this[].map!(n => n.toString()).join('/'); - else ret = this[].map!(n => n.toString()).join('/'); - if (endsWithSlash) ret ~= '/'; - return ret; - } else return m_path; - } - } - + /// Computes a hash sum, enabling storage within associative arrays. hash_t toHash() const nothrow @trusted { - hash_t ret; - auto strhash = &typeid(string).getHash; - try foreach (n; this.save) ret ^= strhash(&n.m_name); - catch (Throwable) assert(false); - if (this.absolute) ret ^= 0xfe3c1738; - if (this.endsWithSlash) ret ^= 0x6aa4352d; - return ret; + try return typeid(string).getHash(&m_path); + catch (Exception e) assert(false, "getHash for string throws!?"); } - string toNativeString() const nothrow { return toString(PathType.native); } + /** Compares two path objects. - Path opSlice() const nothrow @nogc { return this; } + 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; } - 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); + /** 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.bySegment.map!(n => cast(P.Segment)n)); } - size_t opDollar() const nothrow @nogc { return m_length; } + /** Concatenates two paths. - PathEntry opIndex(size_t idx) const nothrow @nogc { auto ret = this[]; ret.popFrontExactly(idx); return ret.front; } - - Path opBinary(string op : "~")(string subpath) const nothrow { return this ~ Path(subpath); } - Path opBinary(string op : "~")(Path subpath) const nothrow { + 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 : "~", F)(GenericPath!F.Segment subpath) const { return this ~ cast(Segment)(subpath); } + /// ditto + GenericPath opBinary(string op : "~")(GenericPath subpath) const nothrow { assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path."); - - if (this.endsWithSlash) - return Path(m_path ~ subpath.toString(m_type), m_type); - if (!m_path.length) return subpath.m_type == m_type ? subpath : Path(subpath.toString(m_type), m_type); - - final switch (m_type) { - case PathType.inet, PathType.posix: - return Path(m_path ~ '/' ~ subpath.toString(m_type), m_type); - case PathType.windows: - return Path(m_path ~ '\\' ~ subpath.toString(m_type), m_type); - } + if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path); + else return GenericPath.fromTrustedString(m_path ~ Format.defaultSeparator ~ subpath.m_path); } - - Path opBinary(string op : "~", R)(R entries) const nothrow - if (!is(R == Path) && isInputRange!R && is(ElementType!R == PathEntry)) + /// 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)) { - 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); - } + return this ~ GenericPath(entries); } + /// Appends a relative path to this path. 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(); - } + deprecated("Use .bySegment together with std.algorithm.searching.startsWith instead.") + bool startsWith(GenericPath prefix) + { + return bySegment.map!(n => n.name).startsWith(prefix.bySegment.map!(n => n.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)])); - } + 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 @@ -353,7 +622,7 @@ unittest { auto unc = "\\\\server\\share\\path"; - auto uncp = Path(unc, PathType.windows); + auto uncp = WindowsPath(unc); assert(uncp.absolute); uncp.normalize(); version(Windows) assert(uncp.toNativeString() == unc); @@ -363,48 +632,43 @@ unittest { auto abspath = "/test/path/"; - auto abspathp = Path(abspath, PathType.posix); + auto abspathp = PosixPath(abspath); 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"); + alias S = PosixPath.Segment; + assert(abspathp.bySegment.equal([S("", '/'), S("test", '/'), S("path", '/')])); } { auto relpath = "test/path/"; - auto relpathp = Path(relpath, PathType.posix); + auto relpathp = PosixPath(relpath); assert(relpathp.toString() == relpath); - version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); + 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"); + alias S = PosixPath.Segment; + assert(relpathp.bySegment.equal([S("test", '/'), S("path", '/')])); } { auto winpath = "C:\\windows\\test"; - auto winpathp = Path(winpath, PathType.windows); + auto winpathp = WindowsPath(winpath); assert(winpathp.toString() == "C:\\windows\\test"); - assert(winpathp.toString(PathType.posix) == "/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); - assert(winpathp.length == 3); - assert(winpathp[0] == "C:"); - assert(winpathp[1] == "windows"); - assert(winpathp[2] == "test"); + alias S = WindowsPath.Segment; + assert(winpathp.bySegment.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")])); } { auto dotpath = "/test/../test2/././x/y"; - auto dotpathp = Path(dotpath, PathType.posix); + auto dotpathp = PosixPath(dotpath); assert(dotpathp.toString() == "/test/../test2/././x/y"); dotpathp.normalize(); assert(dotpathp.toString() == "/test2/x/y", dotpathp.toString()); @@ -412,191 +676,599 @@ unittest { auto dotpath = "/test/..////test2//./x/y"; - auto dotpathp = Path(dotpath, PathType.posix); + auto dotpathp = PosixPath(dotpath); 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); - } + 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/"); - { // 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)); - } + assert(NativePath("").empty); - { // 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("C:\\Windows", PathType.windows).absolute); - assert(Path("C:\\Windows", PathType.windows).toString(PathType.inet) == "/C:/Windows"); - assert((Path("C:\\Windows", PathType.windows) ~ Path("test/this", PathType.inet)).toString() == "C:\\Windows\\test\\this"); - assert(Path("/C:/Windows", PathType.inet).absolute); - assert(Path("/C:/Windows", PathType.inet).toString(PathType.windows) == "C:\\Windows"); - assert((Path("/C:/Windows", PathType.inet) ~ Path("test\\this", PathType.windows)).toString() == "/C:/Windows/test/this"); - assert((Path("", PathType.inet) ~ Path("foo\\bar", PathType.windows)).toString() == "foo/bar"); - assert(Path("C:\\Windows\\", PathType.windows).toString(PathType.inet) == "/C:/Windows/"); - - 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")); + 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")); } @safe unittest { import std.array : appender; - auto app = appender!(Path[]); - void test1(Path p) { app.put(p); } - void test2(Path[] ps) { app.put(ps); } - //void test3(const(Path) p) { app.put(p); } // DMD issue 17251 - //void test4(const(Path)[] ps) { app.put(ps); } -} - - -struct PathEntry { - import std.string : cmp; - - static PathEntry validateFilename(string fname) - @safe { - import std.string : indexOfAny; - enforce(fname.indexOfAny("/\\") < 0, "File name contains forward or backward slashes: "~fname); - return PathEntry(fname, PathType.inet); - } - - @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() 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); } -} - -enum PathType { - posix, - windows, - inet, - native = isWindows ? windows : posix -} - -private string getAbsolutePrefix(string path, PathType type) -@safe nothrow @nogc { - import std.string : indexOfAny; - import std.algorithm.comparison : among; - - 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; - } + 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 { - 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) == ""); + import std.algorithm.comparison : equal; + import std.exception : assertThrown, assertNotThrown; - 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) == ""); + assertThrown!PathValidationException(WindowsPath.Segment("foo/bar")); + assertThrown!PathValidationException(PosixPath.Segment("foo/bar")); + assertNotThrown!PathValidationException(InetPath.Segment("foo/bar")); - 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) == "\\\\"); + 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); } -version (Windows) private enum isWindows = true; -else private enum isWindows = false; +/// 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)); + } + + foreach (s; segments) { + dst.put(s.name); + if (s.separator) + dst.put(sep(s.separator)); + } + } + + 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:"); + } + +@safe nothrow pure: + enum defaultSeparator = '\\'; + + static bool isSeparator(dchar ch) + @nogc { + import std.algorithm.comparison : among; + return ch.among!('\\', '/') != 0; + } + + 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\\"); + } + + static auto decodeSegment(S)(string segment) + { + static struct R { + S[2] items; + size_t i; + 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)); + } + + unittest { + import std.algorithm.comparison : equal; + 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 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); + } +} + + +/** Implements Unix/Linux path semantics. + + See_also: `WindowsPath` +*/ +struct PosixPathFormat { + static void toString(I, O)(I segments, O dst) + { + foreach (s; segments) { + dst.put(s.name); + if (s.separator != '\0') 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"); + } + +@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); + } + + 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)); + } + + unittest { + import std.algorithm.comparison : equal; + 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\\")])); + } +} + + +/** Implements URI/Internet path semantics. + + See_also: `WindowsPath` +*/ +struct InetPathFormat { + static void toString(I, O)(I segments, O dst) + { + import std.format : formattedWrite; + + foreach (e; segments) { + foreach (char c; e.name) { + switch (c) { + default: + dst.formattedWrite("%%%02X", c); + break; + case 'a': .. case 'z': + case 'A': .. case 'Z': + case '0': .. case '9': + case '-', '.', '_', '~': + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': + case ':', '@': + dst.put(c); + break; + } + } + if (e.separator != '\0') 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"); + } + +@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 { + 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 { + for (size_t i = 0; i < path.length; i++) { + switch (path[i]) { + default: + return "Invalid character in internet path."; + // unreserved + case 'A': .. case 'Z': + case 'a': .. case 'z': + case '0': .. case '9': + 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); + } + + static auto decodeSegment(S)(string segment) + { + import std.array : appender; + import std.format : formattedRead; + import std.range : only; + 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 string urlDecode(string s) @safe nothrow { + auto idx = s.indexOf('%'); + if (idx < 0) return s; + + auto ret = appender!string; + ret.put(s[0 .. idx]); + + for (size_t i = idx; i < s.length; i++) { + if (s[i] == '%') { + assert(i+3 < s.length, "segment string not validated!?"); + ret.put(cast(char)(hexDigit(s[i+1]) << 4 | hexDigit(s[i+2]))); + i += 2; + } else ret.put(s[i]); + } + + return ret.data; + } + + if (!segment.length) return only(S.fromTrustedString(null)); + if (segment[$-1] == '/') + return only(S.fromTrustedString(urlDecode(segment[0 .. $-1]), '/')); + return only(S.fromTrustedString(urlDecode(segment))); + } + + unittest { + import std.algorithm.comparison : equal; + 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\\")])); + } +}