diff --git a/dub.sdl b/dub.sdl index 8e9716359..eca2c4fab 100644 --- a/dub.sdl +++ b/dub.sdl @@ -32,12 +32,15 @@ configuration "library" { } configuration "library-nonet" { + + dependency "vibe-core" version="~>2" optional=true dependency "vibe-d:http" version=">=0.9.0 <0.11.0" optional=true targetType "library" excludedSourceFiles "source/app.d" } configuration "dynamic-library-nonet" { + dependency "vibe-core" version="~>2" optional=true dependency "vibe-d:http" version=">=0.9.0 <0.11.0" optional=true targetType "dynamicLibrary" excludedSourceFiles "source/app.d" diff --git a/source/dub/internal/vibecompat/inet/path.d b/source/dub/internal/vibecompat/inet/path.d index 792798203..b398e3d97 100644 --- a/source/dub/internal/vibecompat/inet/path.d +++ b/source/dub/internal/vibecompat/inet/path.d @@ -1,7 +1,7 @@ /** Contains routines for high level path handling. - Copyright: © 2012 rejectedsoftware e.K. + Copyright: © 2012-2021 Sönke Ludwig License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ @@ -10,364 +10,913 @@ module dub.internal.vibecompat.inet.path; version (Have_vibe_core) public import vibe.core.path; else: -import std.algorithm; -import std.array; -import std.conv; -import std.exception; -import std.string; +import std.algorithm.searching : commonPrefix, endsWith, startsWith; +import std.algorithm.comparison : equal, min; +import std.algorithm.iteration : map; +import std.exception : enforce; +import std.range : empty, front, popFront, popFrontExactly, takeExactly; +import std.range.primitives : ElementType, isInputRange, isOutputRange, isForwardRange, save; +import std.traits : isArray, isInstanceOf, isSomeChar; +import std.utf : byChar; -deprecated("Use NativePath instead.") -alias Path = NativePath; +/** Computes the relative path from `base_path` to this path. -/** - Represents an absolute or relative file system path. + Params: + path = The destination path + base_path = The path from which the relative path starts - This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks - are done to disallow invalid operations such as concatenating two absolute paths. It also - validates path strings and allows for easy checking of malicious relative paths. + See_also: `relativeToWeb` */ -struct NativePath { - private { - immutable(PathEntry)[] m_nodes; - bool m_absolute = false; - bool m_endsWithSlash = false; +Path relativeTo(Path)(in Path path, in Path base_path) @safe + if (isInstanceOf!(GenericPath, Path)) +{ + import std.array : array, replicate; + import std.range : chain, drop, take; + + assert(base_path.absolute, "Base path must be absolute for relativeTo."); + assert(path.absolute, "Path must be absolute for relativeTo."); + + if (is(Path == WindowsPath)) { // FIXME: this shouldn't be a special case here! + bool samePrefix(size_t n) + { + return path.bySegment.map!(n => n.encodedName).take(n).equal(base_path.bySegment.map!(n => n.encodedName).take(n)); + } + // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case + auto pref = path.bySegment; + if (!pref.empty && pref.front.encodedName == "") { + pref.popFront(); + if (!pref.empty) { + // different drive? + if (pref.front.encodedName.endsWith(':') && !samePrefix(2)) + return path; + // different UNC path? + if (pref.front.encodedName == "" && !samePrefix(4)) + return path; + } + } } - alias Segment = PathEntry; + auto nodes = path.bySegment; + auto base_nodes = base_path.bySegment; - alias bySegment = nodes; + // skip and count common prefix + size_t base = 0; + while (!nodes.empty && !base_nodes.empty && equal(nodes.front.name, base_nodes.front.name)) { + nodes.popFront(); + base_nodes.popFront(); + base++; + } - /// Constructs a NativePath object by parsing a path string. - this(string pathstr) - { - m_nodes = splitPath(pathstr); - m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && (m_nodes[0].toString().countUntil(':')>0 || m_nodes[0] == "\\")); - m_endsWithSlash = pathstr.endsWith("/"); + enum up = Path.Segment2("..", Path.defaultSeparator); + auto ret = Path(base_nodes.map!(p => up).chain(nodes)); + if (path.endsWithSlash) { + if (ret.empty) return Path.fromTrustedString("." ~ path.toString()[$-1]); + else ret.endsWithSlash = true; } + return ret; +} + +/// +unittest { + import std.array : array; + import std.conv : to; + assert(PosixPath("/some/path").relativeTo(PosixPath("/")) == PosixPath("some/path")); + assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path/")) == PosixPath("../../path/")); + assert(PosixPath("/some/path/").relativeTo(PosixPath("/some/other/path")) == PosixPath("../../path/")); + + assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("C:\\")) == WindowsPath("some\\path")); + assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); + assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); + + assert(WindowsPath("\\\\server\\share\\some\\path").relativeTo(WindowsPath("\\\\server\\share\\")) == WindowsPath("some\\path")); + assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path/")) == WindowsPath("..\\..\\path\\")); + assert(WindowsPath("\\\\server\\share\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share\\some\\other\\path")) == WindowsPath("..\\..\\path\\")); + + assert(WindowsPath("C:\\some\\path").relativeTo(WindowsPath("D:\\")) == WindowsPath("C:\\some\\path")); + assert(WindowsPath("C:\\some\\path\\").relativeTo(WindowsPath("\\\\server\\share")) == WindowsPath("C:\\some\\path\\")); + assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("C:\\some\\other\\path")) == WindowsPath("\\\\server\\some\\path\\")); + assert(WindowsPath("\\\\server\\some\\path\\").relativeTo(WindowsPath("\\\\otherserver\\path")) == WindowsPath("\\\\server\\some\\path\\")); + assert(WindowsPath("\\some\\path\\").relativeTo(WindowsPath("\\other\\path")) == WindowsPath("..\\..\\some\\path\\")); + + assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share\\path2")) == WindowsPath("..\\path1")); + assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); + assert(WindowsPath("\\\\server\\share\\path1").relativeTo(WindowsPath("\\\\server2\\share2\\path2")) == WindowsPath("\\\\server\\share\\path1")); +} - /// Constructs a path object from a list of PathEntry objects. - this(immutable(PathEntry)[] nodes, bool absolute = false) +unittest { { - m_nodes = nodes; - m_absolute = absolute; + 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); } - /// Constructs a relative path with one path entry. - this(PathEntry entry){ - m_nodes = [entry]; - m_absolute = false; + { // 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")); } - /// Determines if the path is absolute. - @property bool absolute() const scope @safe pure nothrow @nogc { return m_absolute; } + { // 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() == "./"); + } - /// Resolves all '.' and '..' path entries as far as possible. - void normalize() - { - immutable(PathEntry)[] newnodes; - foreach( n; m_nodes ){ - switch(n.toString()){ - default: - newnodes ~= n; - break; - case "", ".": break; - case "..": - enforce(!m_absolute || newnodes.length > 0, "Path goes below root node."); - if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; - else newnodes ~= n; - break; - } - } - m_nodes = newnodes; + { + immutable PosixPath p1 = PosixPath("/foo/bar"); + immutable PosixPath p2 = PosixPath("/foo"); + immutable PosixPath result = p1.relativeTo(p2); + assert(result == PosixPath("bar")); + } +} + +nothrow unittest { + auto p1 = PosixPath.fromTrustedString("/foo/bar/baz"); + auto p2 = PosixPath.fromTrustedString("/foo/baz/bam"); + assert(p2.relativeTo(p1).toString == "../../baz/bam"); +} + + +/** Computes the relative path to this path from `base_path` using web path rules. + + The difference to `relativeTo` is that a path not ending in a slash + will not be considered as a path to a directory and the parent path + will instead be used. + + Params: + path = The destination path + base_path = The path from which the relative path starts + + See_also: `relativeTo` +*/ +Path relativeToWeb(Path)(Path path, Path base_path) @safe + if (isInstanceOf!(GenericPath, Path)) +{ + if (!base_path.endsWithSlash) { + assert(base_path.absolute, "Base path must be absolute for relativeToWeb."); + if (base_path.hasParentPath) base_path = base_path.parentPath; + else base_path = Path("/"); + assert(base_path.absolute); } + return path.relativeTo(base_path); +} - /// Converts the Path back to a string representation using slashes. - string toString() - const @safe { - if( m_nodes.empty ) return absolute ? "/" : ""; +/// +/+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/")); +}+/ - Appender!string ret; - // for absolute paths start with / - version(Windows) - { - // Make sure windows path isn't "DRIVE:" - if( absolute && !m_nodes[0].toString().endsWith(':') ) - ret.put('/'); +/** Converts a path to its system native string representation. +*/ +string toNativeString(P)(P path) +{ + return (cast(NativePath)path).toString(); +} + + +/// Represents a path on Windows operating systems. +alias WindowsPath = GenericPath!WindowsPathFormat; + +/// Represents a path on Unix/Posix systems. +alias PosixPath = GenericPath!PosixPathFormat; + +/// Represents a path as part of an URI. +alias InetPath = GenericPath!InetPathFormat; + +/// The path type native to the target operating system. +version (Windows) alias NativePath = WindowsPath; +else alias NativePath = PosixPath; + +/// Provides a common interface to operate on paths of various kinds. +struct GenericPath(F) { +@safe: + alias Format = F; + + /// vibe-core 1.x compatibility alias + alias Segment2 = Segment; + + /** A single path segment. + */ + static struct Segment { + @safe: + + private { + string m_encodedName; + char m_separator = 0; } - else + + /** 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') { - if( absolute ) - { - ret.put('/'); + import std.algorithm.searching : any; + + enforce!PathValidationException(separator == '\0' || Format.isSeparator(separator), + "Invalid path separator."); + auto err = Format.validateDecodedSegment(name); + enforce!PathValidationException(err is null, err); + + m_encodedName = Format.encodeSegment(name); + m_separator = separator; + } + + /** Constructs a path segment without performing validation. + + Note that in debug builds, there are still assertions in place + that verify that the provided values are valid. + + Params: + name = The raw (unencoded) name of the path segment + separator = Optional trailing path separator (e.g. `'/'`) + */ + static Segment fromTrustedString(string name, char separator = '\0') + nothrow pure { + import std.algorithm.searching : any; + assert(separator == '\0' || Format.isSeparator(separator)); + assert(Format.validateDecodedSegment(name) is null, "Invalid path segment."); + return fromTrustedEncodedString(Format.encodeSegment(name), separator); + } + + /** Constructs a path segment without performing validation. + + Note that in debug builds, there are still assertions in place + that verify that the provided values are valid. + + Params: + encoded_name = The encoded name of the path segment + separator = Optional trailing path separator (e.g. `'/'`) + */ + static Segment fromTrustedEncodedString(string encoded_name, char separator = '\0') + nothrow @nogc pure { + import std.algorithm.searching : any; + import std.utf : byCodeUnit; + + assert(separator == '\0' || Format.isSeparator(separator)); + assert(!encoded_name.byCodeUnit.any!(c => Format.isSeparator(c))); + assert(Format.validatePath(encoded_name) is null, "Invalid path segment."); + + Segment ret; + ret.m_encodedName = encoded_name; + ret.m_separator = separator; + return ret; + } + + /** The (file/directory) name of the path segment. + + Note: Depending on the path type, this may return a generic range + type instead of `string`. Use `name.to!string` in that + case if you need an actual `string`. + */ + @property auto name() + const nothrow @nogc { + auto ret = Format.decodeSingleSegment(m_encodedName); + + static if (is(typeof(ret) == string)) return ret; + else { + static struct R { + private typeof(ret) m_value; + + @property bool empty() const { return m_value.empty; } + @property R save() const { return R(m_value.save); } + @property char front() const { return m_value.front; } + @property void popFront() { m_value.popFront(); } + @property char back() const { return m_value.back; } + @property void popBack() { m_value.popBack(); } + + string toString() + const @safe nothrow { + import std.conv : to; + try return m_value.save.to!string; + catch (Exception e) assert(false, e.msg); + } + } + + return R(ret); } } - foreach( i, f; m_nodes ){ - if( i > 0 ) ret.put('/'); - ret.put(f.toString()); + unittest { + import std.conv : to; + auto path = InetPath("/foo%20bar"); + assert(path.head.encodedName == "foo%20bar"); + assert(path.head.name.toString() == "foo bar"); + assert(path.head.name.to!string == "foo bar"); + assert(path.head.name.equal("foo bar")); } - if( m_nodes.length > 0 && m_endsWithSlash ) - ret.put('/'); - return ret.data; - } + /// The encoded representation of the path segment name + @property string encodedName() const nothrow @nogc { return m_encodedName; } + /// The trailing separator (e.g. `'/'`) or `'\0'`. + @property char separator() const nothrow @nogc { return m_separator; } + /// ditto + @property void separator(char ch) { + enforce!PathValidationException(ch == '\0' || Format.isSeparator(ch), + "Character is not a valid path separator."); + m_separator = ch; + } + /// Returns `true` $(I iff) the segment has a trailing path separator. + @property bool hasSeparator() const nothrow @nogc { return m_separator != '\0'; } - /// Converts the NativePath object to a native path string (backslash as path separator on Windows). - string toNativeString() - const { - if (m_nodes.empty) { - version(Windows) { - assert(!absolute, "Empty absolute path detected."); - return m_endsWithSlash ? ".\\" : "."; - } else return absolute ? "/" : m_endsWithSlash ? "./" : "."; + + /** The extension part of the file name. + + If the file name contains an extension, this returns a forward range + with the extension including the leading dot. Otherwise an empty + range is returned. + + See_also: `stripExtension` + */ + @property auto extension() + const nothrow @nogc { + return .extension(this.name); + } + + /// + unittest { + assert(PosixPath("/foo/bar.txt").head.extension.equal(".txt")); + assert(PosixPath("/foo/bar").head.extension.equal("")); + assert(PosixPath("/foo/.bar").head.extension.equal("")); + assert(PosixPath("/foo/.bar.txt").head.extension.equal(".txt")); + } + + + /** Returns the file base name, excluding the extension. + + See_also: `extension` + */ + @property auto withoutExtension() + const nothrow @nogc { + return .stripExtension(this.name); + } + + /// + unittest { + assert(PosixPath("/foo/bar.txt").head.withoutExtension.equal("bar")); + assert(PosixPath("/foo/bar").head.withoutExtension.equal("bar")); + assert(PosixPath("/foo/.bar").head.withoutExtension.equal(".bar")); + assert(PosixPath("/foo/.bar.txt").head.withoutExtension.equal(".bar")); } - Appender!string ret; - // for absolute unix paths start with / - version(Posix) { if(absolute) ret.put('/'); } + /** 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)() + const { + import std.array : array; + + char dsep = '\0'; + if (m_separator) { + if (F.isSeparator(m_separator)) dsep = m_separator; + else dsep = F.defaultSeparator; + } + static if (is(typeof(this.name) == string)) + string n = this.name; + else + string n = this.name.array; + return GenericPath!F.Segment(n, dsep); + } - foreach( i, f; m_nodes ){ - version(Windows) { if( i > 0 ) ret.put('\\'); } - else version(Posix) { if( i > 0 ) ret.put('/'); } - else { static assert(0, "Unsupported OS"); } - ret.put(f.toString()); + /// Compares two path segment names + bool opEquals(Segment other) + const nothrow @nogc { + try return equal(this.name, other.name) && this.hasSeparator == other.hasSeparator; + catch (Exception e) assert(false, e.msg); } + /// ditto + bool opEquals(string name) + const nothrow @nogc { + import std.utf : byCodeUnit; + try return equal(this.name, name.byCodeUnit); + catch (Exception e) assert(false, e.msg); + } + } + + private { + string m_path; + } + + /// The default path segment separator character. + enum char defaultSeparator = Format.defaultSeparator; + + /** Constructs a path from its string representation. + + Throws: + A `PathValidationException` is thrown if the given path string + is not valid. + */ + this(string p) + { + auto err = Format.validatePath(p); + enforce!PathValidationException(err is null, err); + m_path = p; + } + + /** Constructs a path from a single path segment. + + This is equivalent to calling the range based constructor with a + single-element range. + */ + this(Segment segment) + { + import std.range : only; + this(only(segment)); + } + + /** 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. - if( m_nodes.length > 0 && m_endsWithSlash ){ - version(Windows) { ret.put('\\'); } - version(Posix) { ret.put('/'); } + 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 { + if (auto val = Format.validatePath(p)) + assert(false, val); + + GenericPath ret; + ret.m_path = p; + return ret; + } + + /// Tests if a certain character is a path segment separator. + static bool isSeparator(dchar ch) { return ch < 0x80 && Format.isSeparator(cast(char)ch); } + + /// Tests if the path is represented by an empty string. + @property bool empty() const nothrow @nogc { return m_path.length == 0; } + + /// Tests if the path is absolute. + @property bool absolute() const nothrow @nogc { return Format.getAbsolutePrefix(m_path).length > 0; } + + /// Determines whether the path ends with a path separator (i.e. represents a folder specifically). + @property bool endsWithSlash() const nothrow @nogc { return m_path.length > 0 && Format.isSeparator(m_path[$-1]); } + /// ditto + @property void endsWithSlash(bool v) + nothrow { + bool ews = this.endsWithSlash; + if (!ews && v) m_path ~= Format.defaultSeparator; + else if (ews && !v) m_path = m_path[0 .. $-1]; // FIXME?: "/test//" -> "/test/" + } + + /// vibe-core 1.x compatibility alias + alias bySegment2 = bySegment; + + /** Iterates over the individual segments of the path. + + Returns a forward range of `Segment`s. + */ + @property auto bySegment() + const { + static struct R { + import std.traits : ReturnType; + + private { + string m_path; + Segment m_front; + } + + private this(string path) + { + m_path = path; + if (m_path.length) { + auto ap = Format.getAbsolutePrefix(m_path); + if (ap.length && !Format.isSeparator(ap[0])) + m_front = Segment.fromTrustedEncodedString(null, Format.defaultSeparator); + else readFront(); + } + } + + @property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment.init; } + + @property R save() { return this; } + + @property Segment front() { return m_front; } + + void popFront() + nothrow { + assert(m_front != Segment.init); + if (m_path.length) readFront(); + else m_front = Segment.init; + } + + private void readFront() + { + auto n = Format.getFrontNode(m_path); + m_path = m_path[n.length .. $]; + + char sep = '\0'; + if (Format.isSeparator(n[$-1])) { + sep = n[$-1]; + n = n[0 .. $-1]; + } + m_front = Segment.fromTrustedEncodedString(n, sep); + assert(m_front != Segment.init); + } } - return ret.data; + return R(m_path); } - /// Tests if `rhs` is an ancestor or the same as this path. - bool startsWith(const NativePath rhs) const { - if( rhs.m_nodes.length > m_nodes.length ) return false; - foreach( i; 0 .. rhs.m_nodes.length ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return false; - return true; + /// + unittest { + InetPath p = "foo/bar/baz"; + assert(p.bySegment.equal([ + InetPath.Segment("foo", '/'), + InetPath.Segment("bar", '/'), + InetPath.Segment("baz") + ])); } - /// Computes the relative path from `parentPath` to this path. - NativePath relativeTo(const NativePath parentPath) const { - assert(this.absolute && parentPath.absolute, "Determining relative path between non-absolute paths."); - version(Windows){ - // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case - if( this.absolute && !this.empty && - (m_nodes[0].toString().endsWith(":") && !parentPath.startsWith(this[0 .. 1]) || - m_nodes[0] == "\\" && !parentPath.startsWith(this[0 .. min(2, $)]))) + + /** Iterates over the path by segment, each time returning the sub path + leading to that segment. + */ + @property auto byPrefix() + const nothrow @nogc { + static struct R { + import std.traits : ReturnType; + + private { + string m_path; + string m_remainder; + } + + private this(string path) + { + m_path = path; + m_remainder = path; + if (m_path.length) { + auto ap = Format.getAbsolutePrefix(m_path); + if (ap.length && !Format.isSeparator(ap[0])) + m_remainder = m_remainder[ap.length .. $]; + else popFront(); + } + } + + @property bool empty() const nothrow @nogc + { + return m_path.length == 0; + } + + @property R save() { return this; } + + @property GenericPath front() + { + return GenericPath.fromTrustedString(m_path[0 .. $-m_remainder.length]); + } + + void popFront() + nothrow { + assert(m_remainder.length > 0 || m_path.length > 0); + if (m_remainder.length) readFront(); + else m_path = ""; + } + + private void readFront() { - return this; + auto n = Format.getFrontNode(m_remainder); + m_remainder = m_remainder[n.length .. $]; } } - int nup = 0; - while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ - nup++; - } - assert(m_nodes.length >= parentPath.length - nup); - NativePath ret = NativePath(null, false); - assert(m_nodes.length >= parentPath.length - nup); - ret.m_endsWithSlash = true; - foreach( i; 0 .. nup ) ret ~= ".."; - ret ~= NativePath(m_nodes[parentPath.length-nup .. $], false); - ret.m_endsWithSlash = this.m_endsWithSlash; - return ret; + + return R(m_path); + } + + /// + unittest { + assert(InetPath("foo/bar/baz").byPrefix + .equal([ + InetPath("foo/"), + InetPath("foo/bar/"), + InetPath("foo/bar/baz") + ])); + + assert(InetPath("/foo/bar").byPrefix + .equal([ + InetPath("/"), + InetPath("/foo/"), + InetPath("/foo/bar"), + ])); } - /// The last entry of the path - @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0, "Getting head of empty path."); return m_nodes[$-1]; } + // vibe-core 1.x compatibility alias + alias head2 = head; + + /// Returns the trailing segment of the path. + @property Segment head() + const @nogc { + auto n = Format.getBackNode(m_path); + char sep = '\0'; + if (n.length > 0 && Format.isSeparator(n[$-1])) { + sep = n[$-1]; + n = n[0 .. $-1]; + } + return Segment.fromTrustedEncodedString(n, sep); + } - /// The parent path - @property NativePath parentPath() const { return this[0 .. length-1]; } - /// Forward compatibility with vibe-d - @property bool hasParentPath() const { return length > 1; } + /** Determines if the `parentPath` property is valid. + */ + @property bool hasParentPath() + const @nogc { + auto b = Format.getBackNode(m_path); + return b.length < m_path.length; + } - /// The list of path entries of which this path is composed - @property immutable(PathEntry)[] nodes() const { return m_nodes; } + /** Returns a prefix of this path, where the last segment has been dropped. + + Throws: + An `Exception` is thrown if this path has no parent path. Use + `hasParentPath` to test this upfront. + */ + @property GenericPath parentPath() + const @nogc { + auto b = Format.getBackNode(m_path); + () @trusted { + static __gshared e = new Exception("Path has no parent path"); + if (b.length >= m_path.length) throw e; + } (); + return GenericPath.fromTrustedString(m_path[0 .. $ - b.length]); + } - /// The number of path entries of which this path is composed - @property size_t length() const scope @safe pure nothrow @nogc { return m_nodes.length; } - /// True if the path contains no entries - @property bool empty() const scope @safe pure nothrow @nogc { return m_nodes.length == 0; } + /** The extension part of the file name pointed to by the path. - /// Determines if the path ends with a slash (i.e. is a directory) - @property bool endsWithSlash() const { return m_endsWithSlash; } - /// ditto - @property void endsWithSlash(bool v) { m_endsWithSlash = v; } + If the path is not empty and its head segment has an extension, this + returns a forward range with the extension including the leading dot. + Otherwise an empty range is returned. + + See `Segment.extension` for a full description. - /// Determines if this path goes outside of its base path (i.e. begins with '..'). - @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } + See_also: `Segment.extension`, `Segment.stripExtension` + */ + @property auto fileExtension() + const nothrow @nogc { + if (this.empty) return typeof(this.head.extension).init; + return this.head.extension; + } - ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } - NativePath opSlice(size_t start, size_t end) const { - auto ret = NativePath(m_nodes[start .. end], start == 0 ? absolute : false); - if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; + + /** Returns the normalized form of the path. + + See `normalize` for a full description. + */ + @property GenericPath normalized() + const { + GenericPath ret = this; + ret.normalize(); return ret; } - size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } + unittest { + assert(PosixPath("foo/../bar").normalized == PosixPath("bar")); + assert(PosixPath("foo//./bar/../baz").normalized == PosixPath("foo/baz")); + } + + + /** Removes any redundant path segments and replaces all separators by the + default one. + + The resulting path representation is suitable for basic semantic + comparison to other normalized paths. - NativePath opBinary(string OP)(const NativePath rhs) const if( OP == "~" ) { - NativePath ret; - ret.m_nodes = m_nodes; - ret.m_absolute = m_absolute; - ret.m_endsWithSlash = rhs.m_endsWithSlash; - ret.normalize(); // needed to avoid "."~".." become "" instead of ".." + 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. - assert(!rhs.absolute, "Trying to append absolute path: " ~ - this.toNativeString() ~ " ~ " ~ rhs.toNativeString()); - foreach(folder; rhs.m_nodes){ - switch(folder.toString()){ - default: ret.m_nodes = ret.m_nodes ~ folder; break; + Throws: + Throws an `Exception` if an absolute path contains parent directory + segments ("..") that lead to a path that is a parent path of the + root path. + */ + void normalize() + { + import std.array : appender, join; + + Segment[] newnodes; + bool got_non_sep = false; + foreach (n; this.bySegment) { + if (n.hasSeparator) n.separator = Format.defaultSeparator; + if (!got_non_sep) { + if (n.encodedName == "") newnodes ~= n; + else got_non_sep = true; + } + switch (n.encodedName) { + default: newnodes ~= n; break; case "", ".": break; case "..": - enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); - if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) - ret.m_nodes = ret.m_nodes[0 .. $-1]; - else ret.m_nodes = ret.m_nodes ~ folder; + enforce(!this.absolute || newnodes.length > 0, "Path goes below root node."); + if (newnodes.length > 0 && newnodes[$-1].encodedName != "..") newnodes = newnodes[0 .. $-1]; + else newnodes ~= n; break; } } - return ret; + + auto dst = appender!string; + Format.toString(newnodes, dst); + m_path = dst.data; } - NativePath opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } - NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } - void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } - void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } - void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } + /// + unittest { + auto path = WindowsPath("C:\\test/foo/./bar///../baz"); + path.normalize(); + assert(path.toString() == "C:\\test\\foo\\baz", path.toString()); - /// Tests two paths for equality using '=='. - bool opEquals(scope ref const NativePath rhs) const scope @safe { - if( m_absolute != rhs.m_absolute ) return false; - if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; - if( m_nodes.length != rhs.length ) return false; - foreach( i; 0 .. m_nodes.length ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return false; - return true; + path = WindowsPath("foo/../../bar/"); + path.normalize(); + assert(path.toString() == "..\\bar\\"); } - /// ditto - bool opEquals(scope const NativePath other) const scope @safe { return opEquals(other); } - - int opCmp(ref const NativePath rhs) const { - if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; - foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return m_nodes[i].opCmp(rhs.m_nodes[i]); - if( m_nodes.length > rhs.m_nodes.length ) return 1; - if( m_nodes.length < rhs.m_nodes.length ) return -1; - return 0; - } - - size_t toHash() - const nothrow @trusted { - size_t ret; - auto strhash = &typeid(string).getHash; - try foreach (n; nodes) ret ^= strhash(&n.m_name); - catch (Exception) assert(false); - if (m_absolute) ret ^= 0xfe3c1738; - if (m_endsWithSlash) ret ^= 0x6aa4352d; - return ret; + + /// Returns the string representation of the path. + string toString() const nothrow @nogc { return m_path; } + + /// Computes a hash sum, enabling storage within associative arrays. + size_t toHash() const nothrow @trusted + { + try return typeid(string).getHash(&m_path); + catch (Exception e) assert(false, "getHash for string throws!?"); } -} -struct PathEntry { - private { - string m_name; + /** Compares two path objects. + + Note that the exact string representation of the two paths will be + compared. To get a basic semantic comparison, the paths must be + normalized first. + */ + bool opEquals(GenericPath other) const @nogc { return this.m_path == other.m_path; } + + /** Converts the path to a different path format. + + Throws: + A `PathValidationException` will be thrown if the path is not + representable in the requested path format. This can happen + especially when converting Posix or Internet paths to windows paths, + since Windows paths cannot contain a number of characters that the + other representations can, in theory. + */ + P opCast(P)() const if (isInstanceOf!(.GenericPath, P)) { + static if (is(P == GenericPath)) return this; + else return P(this.bySegment.map!(n => cast(P.Segment)n)); } - this(string str) - pure { - assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1)); - m_name = str; + /** Concatenates two paths. + + The right hand side must represent a relative path. + */ + GenericPath opBinary(string op : "~")(string subpath) const { return this ~ GenericPath(subpath); } + /// ditto + GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); } + /// ditto + GenericPath opBinary(string op : "~", 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 (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path); + else return GenericPath.fromTrustedString(m_path ~ Format.defaultSeparator ~ subpath.m_path); + } + /// ditto + GenericPath opBinary(string op : "~", F)(GenericPath!F subpath) const if (!is(F == Format)) { return this ~ cast(GenericPath)subpath; } + /// ditto + GenericPath opBinary(string op : "~", R)(R entries) const nothrow + if (isInputRange!R && is(ElementType!R : Segment)) + { + return this ~ GenericPath(entries); } - string toString() const return scope @safe pure nothrow @nogc { return m_name; } + /// Appends a relative path to this path. + void opOpAssign(string op : "~", T)(T op) { this = this ~ op; } - @property string name() const return scope @safe pure nothrow @nogc { return m_name; } + /** Tests whether the given path is a prefix of this path. - NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); } + Any path separators will be ignored during the comparison. + */ + bool startsWith(GenericPath prefix) + const nothrow { + return bySegment.map!(n => n.name).startsWith(prefix.bySegment.map!(n => n.name)); + } +} - bool opEquals(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } - bool opEquals(scope PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } - bool opEquals(string rhs) const scope @safe pure nothrow @nogc { return m_name == rhs; } - int opCmp(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs.m_name); } - int opCmp(string rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs); } +unittest { + assert(PosixPath("hello/world").bySegment.equal([PosixPath.Segment("hello",'/'), PosixPath.Segment("world")])); + assert(PosixPath("/hello/world/").bySegment.equal([PosixPath.Segment("",'/'), PosixPath.Segment("hello",'/'), PosixPath.Segment("world",'/')])); + assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")])); + assert(WindowsPath("hello/world").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world")])); + assert(WindowsPath("/hello/world/").bySegment.equal([WindowsPath.Segment("",'/'), WindowsPath.Segment("hello",'/'), WindowsPath.Segment("world",'/')])); + assert(WindowsPath("hello\\w/orld").bySegment.equal([WindowsPath.Segment("hello",'\\'), WindowsPath.Segment("w",'/'), WindowsPath.Segment("orld")])); + assert(WindowsPath("hello/w\\orld").bySegment.equal([WindowsPath.Segment("hello",'/'), WindowsPath.Segment("w",'\\'), WindowsPath.Segment("orld")])); + + assert(PosixPath("hello/world").byPrefix.equal([PosixPath("hello/"), PosixPath("hello/world")])); + assert(PosixPath("/hello/world/").byPrefix.equal([PosixPath("/"), PosixPath("/hello/"), PosixPath("/hello/world/")])); + assert(WindowsPath("C:\\Windows").byPrefix.equal([WindowsPath("C:\\"), WindowsPath("C:\\Windows")])); } -/// Joins two path strings. sub-path must be relative. -string joinPath(string basepath, string subpath) +unittest { - NativePath p1 = NativePath(basepath); - NativePath p2 = NativePath(subpath); - return (p1 ~ p2).toString(); -} + { + auto dotpath = "/test/../test2/././x/y"; + auto dotpathp = PosixPath(dotpath); + assert(dotpathp.toString() == "/test/../test2/././x/y"); + dotpathp.normalize(); + assert(dotpathp.toString() == "/test2/x/y", dotpathp.toString()); + } -/// Splits up a path string into its elements/folders -PathEntry[] splitPath(string path) -pure { - if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; - if( path.empty ) return null; - if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; - - // count the number of path nodes - size_t nelements = 0; - foreach( i, char ch; path ) - if( ch == '\\' || ch == '/' ) - nelements++; - nelements++; - - // reserve space for the elements - auto elements = new PathEntry[nelements]; - size_t eidx = 0; - - // detect UNC path - if(path.startsWith("\\")) { - elements[eidx++] = PathEntry(path[0 .. 1]); - path = path[1 .. $]; + auto dotpath = "/test/..////test2//./x/y"; + auto dotpathp = PosixPath(dotpath); + assert(dotpathp.toString() == "/test/..////test2//./x/y"); + dotpathp.normalize(); + assert(dotpathp.toString() == "/test2/x/y"); } - // read and return the elements - size_t startidx = 0; - foreach( i, char ch; path ) - if( ch == '\\' || ch == '/' ){ - elements[eidx++] = PathEntry(path[startidx .. i]); - startidx = i+1; - } - elements[eidx++] = PathEntry(path[startidx .. $]); - assert(eidx == nelements); - return elements; -} + 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/"); -unittest -{ - NativePath p; - assert(p.toNativeString() == "."); - p.endsWithSlash = true; - version(Windows) assert(p.toNativeString() == ".\\"); - else assert(p.toNativeString() == "./"); - - p = NativePath("test/"); - version(Windows) assert(p.toNativeString() == "test\\"); - else assert(p.toNativeString() == "test/"); - p.endsWithSlash = false; - assert(p.toNativeString() == "test"); -} + assert(NativePath("").empty); + + assert(PosixPath("/") ~ NativePath("foo/bar") == PosixPath("/foo/bar")); + assert(PosixPath("") ~ NativePath("foo/bar") == PosixPath("foo/bar")); + assert(PosixPath("foo") ~ NativePath("bar") == PosixPath("foo/bar")); + assert(PosixPath("foo/") ~ NativePath("bar") == PosixPath("foo/bar")); -unittest -{ { auto unc = "\\\\server\\share\\path"; - auto uncp = NativePath(unc); + auto uncp = WindowsPath(unc); + assert(uncp.absolute); uncp.normalize(); version(Windows) assert(uncp.toNativeString() == unc); assert(uncp.absolute); @@ -376,105 +925,835 @@ unittest { auto abspath = "/test/path/"; - auto abspathp = NativePath(abspath); + auto abspathp = PosixPath(abspath); assert(abspathp.toString() == abspath); version(Windows) {} else assert(abspathp.toNativeString() == abspath); assert(abspathp.absolute); assert(abspathp.endsWithSlash); - assert(abspathp.length == 2); - assert(abspathp[0] == "test"); - assert(abspathp[1] == "path"); + alias S = PosixPath.Segment; + assert(abspathp.bySegment.equal([S("", '/'), S("test", '/'), S("path", '/')])); } { auto relpath = "test/path/"; - auto relpathp = NativePath(relpath); + 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 = NativePath(winpath); - version(Windows) { - assert(winpathp.toString() == "C:/windows/test", winpathp.toString()); - assert(winpathp.toNativeString() == winpath); - } else { - assert(winpathp.toString() == "/C:/windows/test", winpathp.toString()); - assert(winpathp.toNativeString() == "/C:/windows/test"); - } + auto winpathp = WindowsPath(winpath); + assert(winpathp.toString() == "C:\\windows\\test"); + assert((cast(PosixPath)winpathp).toString() == "/C:/windows/test", (cast(PosixPath)winpathp).toString()); + version(Windows) assert(winpathp.toNativeString() == winpath); + else assert(winpathp.toNativeString() == "/C:/windows/test"); assert(winpathp.absolute); assert(!winpathp.endsWithSlash); - 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")])); } +} +@safe unittest { + import std.array : appender; + auto app = appender!(PosixPath[]); + void test1(PosixPath p) { app.put(p); } + void test2(PosixPath[] ps) { app.put(ps); } + //void test3(const(PosixPath) p) { app.put(p); } // DMD issue 17251 + //void test4(const(PosixPath)[] ps) { app.put(ps); } +} + +unittest { + import std.exception : assertThrown, assertNotThrown; + + assertThrown!PathValidationException(WindowsPath.Segment("foo/bar")); + assertThrown!PathValidationException(PosixPath.Segment("foo/bar")); + assertNotThrown!PathValidationException(InetPath.Segment("foo/bar")); + + auto p = InetPath("/foo%2fbar/"); + import std.conv : to; + assert(p.bySegment.equal([InetPath.Segment("",'/'), InetPath.Segment("foo/bar",'/')]), p.bySegment.to!string); + p ~= InetPath.Segment("baz/bam"); + assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); +} + +unittest { + assert(!PosixPath("").hasParentPath); + assert(!PosixPath("/").hasParentPath); + assert(!PosixPath("foo\\bar").hasParentPath); + assert(PosixPath("foo/bar").parentPath.toString() == "foo/"); + assert(PosixPath("./foo").parentPath.toString() == "./"); + assert(PosixPath("./foo").parentPath.toString() == "./"); + + assert(!WindowsPath("").hasParentPath); + assert(!WindowsPath("/").hasParentPath); + assert(WindowsPath("foo\\bar").parentPath.toString() == "foo\\"); + assert(WindowsPath("foo/bar").parentPath.toString() == "foo/"); + assert(WindowsPath("./foo").parentPath.toString() == "./"); + assert(WindowsPath("./foo").parentPath.toString() == "./"); + + assert(!InetPath("").hasParentPath); + assert(!InetPath("/").hasParentPath); + assert(InetPath("foo/bar").parentPath.toString() == "foo/"); + assert(InetPath("foo/bar%2Fbaz").parentPath.toString() == "foo/"); + assert(InetPath("./foo").parentPath.toString() == "./"); + assert(InetPath("./foo").parentPath.toString() == "./"); +} + +unittest { + assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar"); +} + +unittest { + assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar"); +} + +/// Thrown when an invalid string representation of a path is detected. +class PathValidationException : Exception { + this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null) + pure nothrow @nogc @safe { - auto dotpath = "/test/../test2/././x/y"; - auto dotpathp = NativePath(dotpath); - assert(dotpathp.toString() == "/test/../test2/././x/y"); - dotpathp.normalize(); - assert(dotpathp.toString() == "/test2/x/y"); + 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)) { - auto dotpath = "/test/..////test2//./x/y"; - auto dotpathp = NativePath(dotpath); - assert(dotpathp.toString() == "/test/..////test2//./x/y"); - dotpathp.normalize(); - assert(dotpathp.toString() == "/test2/x/y"); + char sep(char s) { return isSeparator(s) ? s : defaultSeparator; } + + if (segments.empty) return; + + if (segments.front.name == "" && segments.front.separator) { + auto s = segments.front.separator; + segments.popFront(); + if (segments.empty || !segments.front.name.endsWith(":")) + dst.put(sep(s)); + } + + char lastsep = '\0'; + bool first = true; + foreach (s; segments) { + if (!first || lastsep) dst.put(sep(lastsep)); + else first = false; + dst.put(s.name); + lastsep = s.separator; + } + if (lastsep) dst.put(sep(lastsep)); } + unittest { + import std.array : appender; + struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} + string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } + + assert(str() == ""); + assert(str(Segment("",'/')) == "/"); + assert(str(Segment("",'/'), Segment("foo")) == "/foo"); + assert(str(Segment("",'\\')) == "\\"); + assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); + assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); + assert(str(Segment("",'\\'), Segment("foo",'\\')) == "\\foo\\"); + assert(str(Segment("f oo")) == "f oo"); + assert(str(Segment("",'\\'), Segment("C:")) == "C:"); + assert(str(Segment("",'\\'), Segment("C:", '/')) == "C:/"); + assert(str(Segment("foo",'\\'), Segment("C:")) == "foo\\C:"); + assert(str(Segment("foo"), Segment("bar")) == "foo\\bar"); + } + +@safe nothrow pure: + enum defaultSeparator = '\\'; + + static bool isSeparator(dchar ch) + @nogc { + return ch == '\\' || ch == '/'; + } + + static string getAbsolutePrefix(string path) + @nogc { + if (!path.length) return null; + + if (isSeparator(path[0])) { + return path[0 .. 1]; + } + + foreach (i; 1 .. path.length) + if (isSeparator(path[i])) { + if (path[i-1] == ':') return path[0 .. i+1]; + break; + } + + return path[$-1] == ':' ? path : null; + } + + unittest { + assert(getAbsolutePrefix("test") == ""); + assert(getAbsolutePrefix("test/") == ""); + assert(getAbsolutePrefix("/test") == "/"); + assert(getAbsolutePrefix("\\test") == "\\"); + assert(getAbsolutePrefix("C:\\") == "C:\\"); + assert(getAbsolutePrefix("C:") == "C:"); + assert(getAbsolutePrefix("C:\\test") == "C:\\"); + assert(getAbsolutePrefix("C:\\test\\") == "C:\\"); + assert(getAbsolutePrefix("C:/") == "C:/"); + assert(getAbsolutePrefix("C:/test") == "C:/"); + assert(getAbsolutePrefix("C:/test/") == "C:/"); + assert(getAbsolutePrefix("\\\\server") == "\\"); + assert(getAbsolutePrefix("\\\\server\\") == "\\"); + assert(getAbsolutePrefix("\\\\.\\") == "\\"); + assert(getAbsolutePrefix("\\\\?\\") == "\\"); + } + + static string getFrontNode(string path) + @nogc { + foreach (i; 0 .. path.length) + if (isSeparator(path[i])) + return path[0 .. i+1]; + return path; + } + + unittest { + assert(getFrontNode("") == ""); + assert(getFrontNode("/bar") == "/"); + assert(getFrontNode("foo/bar") == "foo/"); + assert(getFrontNode("foo/") == "foo/"); + assert(getFrontNode("foo") == "foo"); + assert(getFrontNode("\\bar") == "\\"); + assert(getFrontNode("foo\\bar") == "foo\\"); + assert(getFrontNode("foo\\") == "foo\\"); + } + + static string getBackNode(string path) + @nogc { + if (!path.length) return path; + foreach_reverse (i; 0 .. path.length-1) + if (isSeparator(path[i])) + return path[i+1 .. $]; + return path; + } + + unittest { + assert(getBackNode("") == ""); + assert(getBackNode("/bar") == "bar"); + assert(getBackNode("foo/bar") == "bar"); + assert(getBackNode("foo/") == "foo/"); + assert(getBackNode("foo") == "foo"); + assert(getBackNode("\\bar") == "bar"); + assert(getBackNode("foo\\bar") == "bar"); + assert(getBackNode("foo\\") == "foo\\"); + } + + static string decodeSingleSegment(string segment) + @nogc { + assert(segment.length == 0 || segment[$-1] != '/'); + return segment; + } + + unittest { + struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} + assert(decodeSingleSegment("foo") == "foo"); + assert(decodeSingleSegment("fo%20o") == "fo%20o"); + assert(decodeSingleSegment("C:") == "C:"); + assert(decodeSingleSegment("bar:") == "bar:"); + } + + static string validatePath(string path) + @nogc { + import std.algorithm.comparison : among; + + // skip UNC prefix + if (path.startsWith("\\\\")) { + path = path[2 .. $]; + while (path.length && !isSeparator(path[0])) { + if (path[0] < 32 || path[0].among('<', '>', '|')) + return "Invalid character in UNC host name."; + path = path[1 .. $]; + } + if (path.length) path = path[1 .. $]; + } + + // stricter validation for the rest + bool had_sep = false; + foreach (i, char c; path) { + if (c < 32 || c.among!('<', '>', '|', '?')) + return "Invalid character in path."; + if (isSeparator(c)) had_sep = true; + else if (c == ':' && (had_sep || i+1 < path.length && !isSeparator(path[i+1]))) + return "Colon in path that is not part of a drive name."; + + } + return null; + } + + static string validateDecodedSegment(string segment) + @nogc { + auto pe = validatePath(segment); + if (pe) return pe; + foreach (char c; segment) + if (isSeparator(c)) + return "Path segment contains separator character."; + return null; + } + + unittest { + assert(validatePath("c:\\foo") is null); + assert(validatePath("\\\\?\\c:\\foo") is null); + assert(validatePath("//?\\c:\\foo") !is null); + assert(validatePath("-foo/bar\\*\\baz") is null); + assert(validatePath("foo\0bar") !is null); + assert(validatePath("foo\tbar") !is null); + assert(validatePath("\\c:\\foo") !is null); + assert(validatePath("c:d\\foo") !is null); + assert(validatePath("foo\\b:ar") !is null); + assert(validatePath("foo\\bar:\\baz") !is null); + } + + static string encodeSegment(string segment) { - auto parentpath = "/path/to/parent"; - auto parentpathp = NativePath(parentpath); - auto subpath = "/path/to/parent/sub/"; - auto subpathp = NativePath(subpath); - auto subpath_rel = "sub/"; - assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); - auto subfile = "/path/to/parent/child"; - auto subfilep = NativePath(subfile); - auto subfile_rel = "child"; - assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); + assert(segment.length == 0 || segment[$-1] != '/'); + return segment; } +} - { // relative paths across Windows devices are not allowed - version (Windows) { - auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute); - auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute); - auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute); - auto p4 = NativePath("C:\\somepath"); assert(p4.absolute); - auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute); - auto p6 = NativePath("D:\\somepath"); assert(p6.absolute); - assert(p4.relativeTo(p5) == NativePath("../somepath")); - assert(p4.relativeTo(p6) == NativePath("C:\\somepath")); - assert(p4.relativeTo(p1) == NativePath("C:\\somepath")); - assert(p1.relativeTo(p2) == NativePath("../share")); - assert(p1.relativeTo(p3) == NativePath("\\\\server\\share")); - assert(p1.relativeTo(p4) == NativePath("\\\\server\\share")); + +/** Implements Unix/Linux path semantics. + + See_also: `WindowsPath` +*/ +struct PosixPathFormat { + static void toString(I, O)(I segments, O dst) + { + char lastsep = '\0'; + bool first = true; + foreach (s; segments) { + if (!first || lastsep) dst.put('/'); + else first = false; + dst.put(s.name); + lastsep = s.separator; } + if (lastsep) dst.put('/'); + } + + unittest { + import std.array : appender; + struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} + string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } + + assert(str() == ""); + assert(str(Segment("",'/')) == "/"); + assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); + assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); + assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); + assert(str(Segment("f oo")) == "f oo"); + assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); + } + +@safe nothrow pure: + enum defaultSeparator = '/'; + + static bool isSeparator(dchar ch) + @nogc { + return ch == '/'; + } + + static string getAbsolutePrefix(string path) + @nogc { + if (path.length > 0 && path[0] == '/') + return path[0 .. 1]; + return null; + } + + unittest { + assert(getAbsolutePrefix("/") == "/"); + assert(getAbsolutePrefix("/test") == "/"); + assert(getAbsolutePrefix("/test/") == "/"); + assert(getAbsolutePrefix("test/") == ""); + assert(getAbsolutePrefix("") == ""); + assert(getAbsolutePrefix("./") == ""); + } + + static string getFrontNode(string path) + @nogc { + import std.string : indexOf; + auto idx = path.indexOf('/'); + return idx < 0 ? path : path[0 .. idx+1]; + } + + unittest { + assert(getFrontNode("") == ""); + assert(getFrontNode("/bar") == "/"); + assert(getFrontNode("foo/bar") == "foo/"); + assert(getFrontNode("foo/") == "foo/"); + assert(getFrontNode("foo") == "foo"); + } + + static string getBackNode(string path) + @nogc { + if (!path.length) return path; + foreach_reverse (i; 0 .. path.length-1) + if (path[i] == '/') + return path[i+1 .. $]; + return path; + } + + unittest { + assert(getBackNode("") == ""); + assert(getBackNode("/bar") == "bar"); + assert(getBackNode("foo/bar") == "bar"); + assert(getBackNode("foo/") == "foo/"); + assert(getBackNode("foo") == "foo"); + } + + static string validatePath(string path) + @nogc { + foreach (char c; path) + if (c == '\0') + return "Invalid NUL character in file name"; + return null; + } + + static string validateDecodedSegment(string segment) + @nogc { + auto pe = validatePath(segment); + if (pe) return pe; + foreach (char c; segment) + if (isSeparator(c)) + return "Path segment contains separator character."; + return null; + } + + unittest { + assert(validatePath("-foo/bar*/baz?") is null); + assert(validatePath("foo\0bar") !is null); + } + + static string decodeSingleSegment(string segment) + @nogc { + assert(segment.length == 0 || segment[$-1] != '/'); + return segment; + } + + unittest { + struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} + assert(decodeSingleSegment("foo") == "foo"); + assert(decodeSingleSegment("fo%20o\\") == "fo%20o\\"); + } + + static string encodeSegment(string segment) + { + assert(segment.length == 0 || segment[$-1] != '/'); + return segment; } } + +/** Implements URI/Internet path semantics. + + See_also: `WindowsPath` +*/ +struct InetPathFormat { + static void toString(I, O)(I segments, O dst) + { + char lastsep = '\0'; + bool first = true; + foreach (e; segments) { + if (!first || lastsep) dst.put('/'); + else first = false; + static if (is(typeof(e.encodedName))) + dst.put(e.encodedName); + else encodeSegment(dst, e.name); + lastsep = e.separator; + } + if (lastsep) dst.put('/'); + } + + unittest { + import std.array : appender; + struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }} + string str(Segment[] segs...) { auto ret = appender!string; toString(segs, ret); return ret.data; } + assert(str() == ""); + assert(str(Segment("",'/')) == "/"); + assert(str(Segment("foo",'/'), Segment("bar",'/')) == "foo/bar/"); + assert(str(Segment("",'/'), Segment("foo",'\0')) == "/foo"); + assert(str(Segment("",'\\'), Segment("foo",'\\')) == "/foo/"); + assert(str(Segment("f oo")) == "f%20oo"); + assert(str(Segment("foo"), Segment("bar")) == "foo/bar"); + } + +@safe pure nothrow: + enum defaultSeparator = '/'; + + static bool isSeparator(dchar ch) + @nogc { + return ch == '/'; + } + + static string getAbsolutePrefix(string path) + @nogc { + if (path.length > 0 && path[0] == '/') + return path[0 .. 1]; + return null; + } + + unittest { + assert(getAbsolutePrefix("/") == "/"); + assert(getAbsolutePrefix("/test") == "/"); + assert(getAbsolutePrefix("/test/") == "/"); + assert(getAbsolutePrefix("test/") == ""); + assert(getAbsolutePrefix("") == ""); + assert(getAbsolutePrefix("./") == ""); + } + + static string getFrontNode(string path) + @nogc { + import std.string : indexOf; + auto idx = path.indexOf('/'); + return idx < 0 ? path : path[0 .. idx+1]; + } + + unittest { + assert(getFrontNode("") == ""); + assert(getFrontNode("/bar") == "/"); + assert(getFrontNode("foo/bar") == "foo/"); + assert(getFrontNode("foo/") == "foo/"); + assert(getFrontNode("foo") == "foo"); + } + + static string getBackNode(string path) + @nogc { + import std.string : lastIndexOf; + + if (!path.length) return path; + ptrdiff_t idx; + try idx = path[0 .. $-1].lastIndexOf('/'); + catch (Exception e) assert(false, e.msg); + if (idx >= 0) return path[idx+1 .. $]; + return path; + } + + unittest { + assert(getBackNode("") == ""); + assert(getBackNode("/bar") == "bar"); + assert(getBackNode("foo/bar") == "bar"); + assert(getBackNode("foo/") == "foo/"); + assert(getBackNode("foo") == "foo"); + } + + static string validatePath(string path) + @nogc { + for (size_t i = 0; i < path.length; i++) { + if (isAsciiAlphaNum(path[i])) + continue; + + switch (path[i]) { + default: + return "Invalid character in internet path."; + // unreserved + case '-', '.', '_', '~': + // subdelims + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': + // additional delims + case ':', '@': + // segment delimiter + case '/': + break; + case '%': // pct encoding + if (path.length < i+3) + return "Unterminated percent encoding sequence in internet path."; + foreach (j; 0 .. 2) { + switch (path[++i]) { + default: return "Invalid percent encoding sequence in internet path."; + case '0': .. case '9': + case 'a': .. case 'f': + case 'A': .. case 'F': + break; + } + } + break; + } + } + return null; + } + + static string validateDecodedSegment(string seg) + @nogc { + return null; + } + + unittest { + assert(validatePath("") is null); + assert(validatePath("/") is null); + assert(validatePath("/test") is null); + assert(validatePath("test") is null); + assert(validatePath("/C:/test") is null); + assert(validatePath("/test%ab") is null); + assert(validatePath("/test%ag") !is null); + assert(validatePath("/test%a") !is null); + assert(validatePath("/test%") !is null); + assert(validatePath("/test§") !is null); + assert(validatePath("föö") !is null); + } + + static auto decodeSingleSegment(string segment) + @nogc { + import std.string : indexOf; + + static int hexDigit(char ch) @safe nothrow @nogc { + assert(ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f'); + if (ch >= '0' && ch <= '9') return ch - '0'; + else if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + else return ch - 'A' + 10; + } + + static struct R { + @safe pure nothrow @nogc: + + private { + string m_str; + } + + this(string s) + { + m_str = s; + } + + @property bool empty() const { return m_str.length == 0; } + + @property R save() const { return this; } + + @property char front() + const { + auto ch = m_str[0]; + if (ch != '%') return ch; + + auto a = m_str[1]; + auto b = m_str[2]; + return cast(char)(16 * hexDigit(a) + hexDigit(b)); + } + + @property void popFront() + { + assert(!empty); + if (m_str[0] == '%') m_str = m_str[3 .. $]; + else m_str = m_str[1 .. $]; + } + + @property char back() + const { + if (m_str.length >= 3 && m_str[$-3] == '%') { + auto a = m_str[$-2]; + auto b = m_str[$-1]; + return cast(char)(16 * hexDigit(a) + hexDigit(b)); + } else return m_str[$-1]; + } + + void popBack() + { + assert(!empty); + if (m_str.length >= 3 && m_str[$-3] == '%') m_str = m_str[0 .. $-3]; + else m_str = m_str[0 .. $-1]; + } + } + + return R(segment); + } + + unittest { + import std.range : retro; + + scope (failure) assert(false); + + assert(decodeSingleSegment("foo").equal("foo")); + assert(decodeSingleSegment("fo%20o\\").equal("fo o\\")); + assert(decodeSingleSegment("foo%20").equal("foo ")); + assert(decodeSingleSegment("foo").retro.equal("oof")); + assert(decodeSingleSegment("fo%20o\\").retro.equal("\\o of")); + assert(decodeSingleSegment("foo%20").retro.equal(" oof")); + } + + + static string encodeSegment(string segment) + { + import std.array : appender; + + foreach (i, char c; segment) { + if (isAsciiAlphaNum(c)) continue; + switch (c) { + default: + auto ret = appender!string; + ret.put(segment[0 .. i]); + encodeSegment(ret, segment[i .. $]); + return ret.data; + case '-', '.', '_', '~': + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': + case ':', '@': + break; + } + } + + return segment; + } + + unittest { + assert(encodeSegment("foo") == "foo"); + assert(encodeSegment("foo bar") == "foo%20bar"); + } + + static void encodeSegment(R)(ref R dst, string segment) + { + static immutable char[16] digit = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + + foreach (char c; segment) { + switch (c) { + default: + dst.put('%'); + dst.put(digit[uint(c) / 16]); + dst.put(digit[uint(c) % 16]); + break; + case 'a': .. case 'z': + case 'A': .. case 'Z': + case '0': .. case '9': + case '-', '.', '_', '~': + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': + case ':', '@': + dst.put(c); + break; + } + } + } +} + +private auto extension(R)(R filename) + if (isForwardRange!R && isSomeChar!(ElementType!R)) +{ + if (filename.empty) return filename; + + static if (isArray!R) { // avoid auto decoding + filename = filename[1 .. $]; // ignore leading dot + + R candidate; + while (filename.length) { + if (filename[0] == '.') + candidate = filename; + filename = filename[1 .. $]; + } + return candidate; + } else { + filename.popFront(); // ignore leading dot + + R candidate; + while (!filename.empty) { + if (filename.front == '.') + candidate = filename.save; + filename.popFront(); + } + return candidate; + } +} + +@safe nothrow unittest { + assert(extension("foo") == ""); + assert(extension("foo.txt") == ".txt"); + assert(extension(".foo") == ""); + assert(extension(".foo.txt") == ".txt"); + assert(extension("foo.bar.txt") == ".txt"); +} + +unittest { + assert(extension(InetPath("foo").head.name).equal("")); + assert(extension(InetPath("foo.txt").head.name).equal(".txt")); + assert(extension(InetPath(".foo").head.name).equal("")); + assert(extension(InetPath(".foo.txt").head.name).equal(".txt")); + assert(extension(InetPath("foo.bar.txt").head.name).equal(".txt")); +} + + +private auto stripExtension(R)(R filename) + if (isForwardRange!R && isSomeChar!(ElementType!R)) +{ + static if (isArray!R) { // make sure to return a slice + if (!filename.length) return filename; + R r = filename; + r = r[1 .. $]; // ignore leading dot + size_t cnt = 0, rcnt = r.length; + while (r.length) { + if (r[0] == '.') + rcnt = cnt; + cnt++; + r = r[1 .. $]; + } + return filename[0 .. rcnt + 1]; + } else { + if (filename.empty) return filename.takeExactly(0); + R r = filename.save; + size_t cnt = 0, rcnt = size_t.max; + r.popFront(); // ignore leading dot + while (!r.empty) { + if (r.front == '.') + rcnt = cnt; + cnt++; + r.popFront(); + } + if (rcnt == size_t.max) return filename.takeExactly(cnt + 1); + return filename.takeExactly(rcnt + 1); + } +} + +@safe nothrow unittest { + assert(stripExtension("foo") == "foo"); + assert(stripExtension("foo.txt") == "foo"); + assert(stripExtension(".foo") == ".foo"); + assert(stripExtension(".foo.txt") == ".foo"); + assert(stripExtension("foo.bar.txt") == "foo.bar"); +} + +unittest { // test range based path + import std.utf : byWchar; + + assert(stripExtension("foo".byWchar).equal("foo")); + assert(stripExtension("foo.txt".byWchar).equal("foo")); + assert(stripExtension(".foo".byWchar).equal(".foo")); + assert(stripExtension(".foo.txt".byWchar).equal(".foo")); + assert(stripExtension("foo.bar.txt".byWchar).equal("foo.bar")); + + assert(stripExtension(InetPath("foo").head.name).equal("foo")); + assert(stripExtension(InetPath("foo.txt").head.name).equal("foo")); + assert(stripExtension(InetPath(".foo").head.name).equal(".foo")); + assert(stripExtension(InetPath(".foo.txt").head.name).equal(".foo")); + assert(stripExtension(InetPath("foo.bar.txt").head.name).equal("foo.bar")); +} + +private static bool isAsciiAlphaNum(char ch) +@safe nothrow pure @nogc { + return (uint(ch) & 0xDF) - 0x41 < 26 || uint(ch) - '0' <= 9; +} + unittest { - assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz"); - assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/"); - assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar"); - assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/"); - assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == ".."); - assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../"); - assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz"); - assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/"); - assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../"); - assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../"); - assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == ""); - assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == ""); + assert(!isAsciiAlphaNum('@')); + assert(isAsciiAlphaNum('A')); + assert(isAsciiAlphaNum('Z')); + assert(!isAsciiAlphaNum('[')); + assert(!isAsciiAlphaNum('`')); + assert(isAsciiAlphaNum('a')); + assert(isAsciiAlphaNum('z')); + assert(!isAsciiAlphaNum('{')); + assert(!isAsciiAlphaNum('/')); + assert(isAsciiAlphaNum('0')); + assert(isAsciiAlphaNum('9')); + assert(!isAsciiAlphaNum(':')); +} + +unittest { // regression tests + assert(NativePath("").bySegment.empty); } diff --git a/source/dub/internal/vibecompat/inet/url.d b/source/dub/internal/vibecompat/inet/url.d index be452cf7a..3192b964d 100644 --- a/source/dub/internal/vibecompat/inet/url.d +++ b/source/dub/internal/vibecompat/inet/url.d @@ -232,9 +232,7 @@ struct URL { } URL opBinary(string OP)(NativePath rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); } - URL opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); } void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { m_path ~= rhs; } - void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; } /// Tests two URLs for equality using '=='. bool opEquals(ref const URL rhs) const { @@ -245,13 +243,6 @@ struct URL { } /// ditto bool opEquals(const URL other) const { return opEquals(other); } - - int opCmp(ref const URL rhs) const { - if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema); - if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host); - if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path); - return true; - } } unittest {