From 07e077a0098be467496f43199de178a4a0dcb6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Tue, 20 Jun 2017 00:37:56 +0200 Subject: [PATCH] Redesign the Path type to statically encode the path format. The previous design, while intended as an improvement over the one-size-fits all Path struct of vibe-d:core, turned out to produce lots of bugs during the transition, because of missing Path.type checks. The new design uses a cleaner approach, where the static type of a path value encodes the path format. An explicit cast is necessary to convert between different path types. The internet path type also performs proper validation and percent encoding, so that InetPath.toString() always produces a valid URI path. --- source/vibe/core/file.d | 74 +- source/vibe/core/path.d | 1500 ++++++++++++++++++++++++++++----------- 2 files changed, 1123 insertions(+), 451 deletions(-) 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\\")])); + } +}