Add GenericPath.bySegment2 and .head2 as fully nogc alternatives.

This commit is contained in:
Sönke Ludwig 2019-09-19 13:38:08 +02:00
parent 32858b7aee
commit 6ef53121ad

View file

@ -1,14 +1,14 @@
/** /**
Contains routines for high level path handling. Contains routines for high level path handling.
Copyright: © 2012-2018 Sönke Ludwig Copyright: © 2012-2019 Sönke Ludwig
License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
Authors: Sönke Ludwig Authors: Sönke Ludwig
*/ */
module vibe.core.path; module vibe.core.path;
import std.algorithm.searching : commonPrefix, endsWith, startsWith; import std.algorithm.searching : commonPrefix, endsWith, startsWith;
import std.algorithm.comparison : min; import std.algorithm.comparison : equal, min;
import std.algorithm.iteration : map; import std.algorithm.iteration : map;
import std.exception : enforce; import std.exception : enforce;
import std.range : empty, front, popFront, popFrontExactly, takeExactly; import std.range : empty, front, popFront, popFrontExactly, takeExactly;
@ -27,7 +27,6 @@ import std.traits : isInstanceOf;
Path relativeTo(Path)(Path path, Path base_path) @safe Path relativeTo(Path)(Path path, Path base_path) @safe
if (isInstanceOf!(GenericPath, Path)) if (isInstanceOf!(GenericPath, Path))
{ {
import std.algorithm.comparison : equal;
import std.array : array, replicate; import std.array : array, replicate;
import std.range : chain, drop, take; import std.range : chain, drop, take;
@ -345,6 +344,201 @@ struct GenericPath(F) {
else m_front = Segment.init; else m_front = Segment.init;
} }
private void readFront()
nothrow {
import std.array : array;
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];
}
static if (is(typeof(Format.decodeSingleSegment(n)) == string))
string ndec = Format.decodeSingleSegment(n);
else
string ndec = Format.decodeSingleSegment(n).array;
m_front = Segment.fromTrustedString(ndec, sep);
assert(m_front != Segment.init);
}
}
/** A single path segment.
*/
static struct Segment2 {
@safe:
private {
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_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 Segment2 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:
name = The encoded name of the path segment
separator = Optional trailing path separator (e.g. `'/'`)
*/
static Segment2 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.");
Segment2 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 { return Format.decodeSingleSegment(m_encodedName); }
/// 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 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.Segment2 opCast(T : GenericPath!F.Segment2, F)()
{
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.Segment2(n, dsep);
}
/// Compares two path segment names
bool opEquals(Segment2 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);
}
}
/** Represents a path as an forward range of `Segment2`s.
Note that in contrast to `PathRange`, this range allows pure `@nogc`
iteration over encoded path segments.
*/
static struct PathRange2 {
import std.traits : ReturnType;
private {
string m_path;
Segment2 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 = Segment2.fromTrustedEncodedString(null, '/');
else readFront();
}
}
@property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment2.init; }
@property PathRange2 save() { return this; }
@property Segment2 front() { return m_front; }
void popFront()
nothrow {
assert(m_front != Segment2.init);
if (m_path.length) readFront();
else m_front = Segment2.init;
}
private void readFront() private void readFront()
{ {
auto n = Format.getFrontNode(m_path); auto n = Format.getFrontNode(m_path);
@ -355,11 +549,12 @@ struct GenericPath(F) {
sep = n[$-1]; sep = n[$-1];
n = n[0 .. $-1]; n = n[0 .. $-1];
} }
m_front = Segment.fromTrustedString(Format.decodeSingleSegment(n), sep); m_front = Segment2.fromTrustedEncodedString(n, sep);
assert(m_front != Segment.init); assert(m_front != Segment2.init);
} }
} }
private { private {
string m_path; string m_path;
} }
@ -390,6 +585,12 @@ struct GenericPath(F) {
import std.range : only; import std.range : only;
this(only(segment)); this(only(segment));
} }
/// ditto
this(Segment2 segment)
{
import std.range : only;
this(only(segment));
}
/** Constructs a path from an input range of `Segment`s. /** Constructs a path from an input range of `Segment`s.
@ -405,6 +606,15 @@ struct GenericPath(F) {
Format.toString(segments, dst); Format.toString(segments, dst);
m_path = dst.data; m_path = dst.data;
} }
/// ditto
this(R)(R segments)
if (isInputRange!R && is(ElementType!R : Segment2))
{
import std.array : appender;
auto dst = appender!string;
Format.toString(segments, dst);
m_path = dst.data;
}
/** Constructs a path from its string representation. /** Constructs a path from its string representation.
@ -451,16 +661,39 @@ struct GenericPath(F) {
/// Iterates over the path by `Segment`. /// Iterates over the path by `Segment`.
@property PathRange bySegment() const { return PathRange(m_path); } @property PathRange bySegment() const { return PathRange(m_path); }
/// Iterates over the path by `Segment`.
@property PathRange2 bySegment2() const { return PathRange2(m_path); }
/// Returns the trailing segment of the path. /// Returns the trailing segment of the path.
@property Segment head() @property Segment head()
const { const {
import std.array : array;
auto n = Format.getBackNode(m_path); auto n = Format.getBackNode(m_path);
char sep = '\0'; char sep = '\0';
if (n.length > 0 && Format.isSeparator(n[$-1])) { if (n.length > 0 && Format.isSeparator(n[$-1])) {
sep = n[$-1]; sep = n[$-1];
n = n[0 .. $-1]; n = n[0 .. $-1];
} }
return Segment.fromTrustedString(Format.decodeSingleSegment(n), sep);
static if (is(typeof(Format.decodeSingleSegment(n)) == string))
string ndec = Format.decodeSingleSegment(n);
else
string ndec = Format.decodeSingleSegment(n).array;
return Segment.fromTrustedString(ndec, sep);
}
/// Returns the trailing segment of the path.
@property Segment2 head2()
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 Segment2.fromTrustedEncodedString(n, sep);
} }
/** Determines if the `parentPath` property is valid. /** Determines if the `parentPath` property is valid.
@ -580,8 +813,12 @@ struct GenericPath(F) {
/// ditto /// ditto
GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); } GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); }
/// ditto /// ditto
GenericPath opBinary(string op : "~")(Segment2 subpath) const { return this ~ GenericPath(subpath); }
/// ditto
GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment subpath) const { return this ~ cast(Segment)(subpath); } GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment subpath) const { return this ~ cast(Segment)(subpath); }
/// ditto /// ditto
GenericPath opBinary(string op : "~", F)(GenericPath!F.Segment2 subpath) const { return this ~ cast(Segment2)(subpath); }
/// ditto
GenericPath opBinary(string op : "~")(GenericPath subpath) const nothrow { GenericPath opBinary(string op : "~")(GenericPath subpath) const nothrow {
assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path."); assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path.");
if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path); if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path);
@ -595,6 +832,12 @@ struct GenericPath(F) {
{ {
return this ~ GenericPath(entries); return this ~ GenericPath(entries);
} }
/// ditto
GenericPath opBinary(string op : "~", R)(R entries) const nothrow
if (isInputRange!R && is(ElementType!R : Segment2))
{
return this ~ GenericPath(entries);
}
/// Appends a relative path to this path. /// Appends a relative path to this path.
void opOpAssign(string op : "~", T)(T op) { this = this ~ op; } void opOpAssign(string op : "~", T)(T op) { this = this ~ op; }
@ -610,8 +853,6 @@ struct GenericPath(F) {
} }
unittest { unittest {
import std.algorithm.comparison : equal;
assert(PosixPath("hello/world").bySegment.equal([PosixPath.Segment("hello",'/'), PosixPath.Segment("world")])); 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("",'/'), PosixPath.Segment("hello",'/'), PosixPath.Segment("world",'/')]));
assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")])); assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")]));
@ -621,10 +862,18 @@ unittest {
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 {
assert(PosixPath("hello/world").bySegment2.equal([PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world")]));
assert(PosixPath("/hello/world/").bySegment2.equal([PosixPath.Segment2("",'/'), PosixPath.Segment2("hello",'/'), PosixPath.Segment2("world",'/')]));
assert(PosixPath("hello\\world").bySegment2.equal([PosixPath.Segment2("hello\\world")]));
assert(WindowsPath("hello/world").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world")]));
assert(WindowsPath("/hello/world/").bySegment2.equal([WindowsPath.Segment2("",'/'), WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("world",'/')]));
assert(WindowsPath("hello\\w/orld").bySegment2.equal([WindowsPath.Segment2("hello",'\\'), WindowsPath.Segment2("w",'/'), WindowsPath.Segment2("orld")]));
assert(WindowsPath("hello/w\\orld").bySegment2.equal([WindowsPath.Segment2("hello",'/'), WindowsPath.Segment2("w",'\\'), WindowsPath.Segment2("orld")]));
}
unittest unittest
{ {
import std.algorithm.comparison : equal;
{ {
auto unc = "\\\\server\\share\\path"; auto unc = "\\\\server\\share\\path";
auto uncp = WindowsPath(unc); auto uncp = WindowsPath(unc);
@ -704,6 +953,55 @@ unittest
assert(PosixPath("foo/") ~ NativePath("bar") == PosixPath("foo/bar")); assert(PosixPath("foo/") ~ NativePath("bar") == PosixPath("foo/bar"));
} }
unittest
{
{
auto unc = "\\\\server\\share\\path";
auto uncp = WindowsPath(unc);
assert(uncp.absolute);
uncp.normalize();
version(Windows) assert(uncp.toNativeString() == unc);
assert(uncp.absolute);
assert(!uncp.endsWithSlash);
}
{
auto abspath = "/test/path/";
auto abspathp = PosixPath(abspath);
assert(abspathp.toString() == abspath);
version(Windows) {} else assert(abspathp.toNativeString() == abspath);
assert(abspathp.absolute);
assert(abspathp.endsWithSlash);
alias S = PosixPath.Segment2;
assert(abspathp.bySegment2.equal([S("", '/'), S("test", '/'), S("path", '/')]));
}
{
auto relpath = "test/path/";
auto relpathp = PosixPath(relpath);
assert(relpathp.toString() == relpath);
version(Windows) assert(relpathp.toNativeString() == "test/path/");
else assert(relpathp.toNativeString() == relpath);
assert(!relpathp.absolute);
assert(relpathp.endsWithSlash);
alias S = PosixPath.Segment2;
assert(relpathp.bySegment2.equal([S("test", '/'), S("path", '/')]));
}
{
auto winpath = "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);
alias S = WindowsPath.Segment2;
assert(winpathp.bySegment2.equal([S("", '/'), S("C:", '\\'), S("windows", '\\'), S("test")]));
}
}
@safe unittest { @safe unittest {
import std.array : appender; import std.array : appender;
auto app = appender!(PosixPath[]); auto app = appender!(PosixPath[]);
@ -714,7 +1012,6 @@ unittest
} }
unittest { unittest {
import std.algorithm.comparison : equal;
import std.exception : assertThrown, assertNotThrown; import std.exception : assertThrown, assertNotThrown;
assertThrown!PathValidationException(WindowsPath.Segment("foo/bar")); assertThrown!PathValidationException(WindowsPath.Segment("foo/bar"));
@ -727,6 +1024,20 @@ unittest {
assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString); assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString);
} }
unittest {
import std.exception : assertThrown, assertNotThrown;
assertThrown!PathValidationException(WindowsPath.Segment2("foo/bar"));
assertThrown!PathValidationException(PosixPath.Segment2("foo/bar"));
assertNotThrown!PathValidationException(InetPath.Segment2("foo/bar"));
auto p = InetPath("/foo%2fbar/");
import std.conv : to;
assert(p.bySegment2.equal([InetPath.Segment2("",'/'), InetPath.Segment2("foo/bar",'/')]), p.bySegment2.to!string);
p ~= InetPath.Segment2("baz/bam");
assert(p.toString() == "/foo%2fbar/baz%2Fbam", p.toString);
}
unittest { unittest {
assert(!PosixPath("").hasParentPath); assert(!PosixPath("").hasParentPath);
assert(!PosixPath("/").hasParentPath); assert(!PosixPath("/").hasParentPath);
@ -754,6 +1065,10 @@ unittest {
assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar"); assert(WindowsPath([WindowsPath.Segment("foo"), WindowsPath.Segment("bar")]).toString() == "foo\\bar");
} }
unittest {
assert(WindowsPath([WindowsPath.Segment2("foo"), WindowsPath.Segment2("bar")]).toString() == "foo\\bar");
}
/// Thrown when an invalid string representation of a path is detected. /// Thrown when an invalid string representation of a path is detected.
class PathValidationException : Exception { class PathValidationException : Exception {
this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null) this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null)
@ -925,7 +1240,6 @@ struct WindowsPathFormat {
} }
unittest { 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); }} 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("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
@ -935,13 +1249,12 @@ struct WindowsPathFormat {
} }
static string decodeSingleSegment(string segment) static string decodeSingleSegment(string segment)
{ @nogc {
assert(segment.length == 0 || segment[$-1] != '/'); assert(segment.length == 0 || segment[$-1] != '/');
return segment; return segment;
} }
unittest { 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); }} 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("foo") == "foo");
assert(decodeSingleSegment("fo%20o") == "fo%20o"); assert(decodeSingleSegment("fo%20o") == "fo%20o");
@ -1002,8 +1315,7 @@ struct WindowsPathFormat {
static string encodeSegment(string segment) static string encodeSegment(string segment)
{ {
assert(segment.length > 0, "Path segment string must not be empty."); assert(segment.length == 0 || segment[$-1] != '/');
assert(segment[$-1] != '/');
return segment; return segment;
} }
} }
@ -1132,7 +1444,6 @@ struct PosixPathFormat {
} }
unittest { 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); }} 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("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
@ -1140,13 +1451,12 @@ struct PosixPathFormat {
} }
static string decodeSingleSegment(string segment) static string decodeSingleSegment(string segment)
{ @nogc {
assert(segment.length == 0 || segment[$-1] != '/'); assert(segment.length == 0 || segment[$-1] != '/');
return segment; return segment;
} }
unittest { 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); }} 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("foo") == "foo");
assert(decodeSingleSegment("fo%20o\\") == "fo%20o\\"); assert(decodeSingleSegment("fo%20o\\") == "fo%20o\\");
@ -1154,8 +1464,7 @@ struct PosixPathFormat {
static string encodeSegment(string segment) static string encodeSegment(string segment)
{ {
assert(segment.length > 0, "Path segment string must not be empty."); assert(segment.length == 0 || segment[$-1] != '/');
assert(segment[$-1] != '/');
return segment; return segment;
} }
} }
@ -1173,7 +1482,9 @@ struct InetPathFormat {
foreach (e; segments) { foreach (e; segments) {
if (!first || lastsep) dst.put('/'); if (!first || lastsep) dst.put('/');
else first = false; else first = false;
encodeSegment(dst, e.name); static if (is(typeof(e.encodedName)))
dst.put(e.encodedName);
else encodeSegment(dst, e.name);
lastsep = e.separator; lastsep = e.separator;
} }
if (lastsep) dst.put('/'); if (lastsep) dst.put('/');
@ -1309,18 +1620,26 @@ struct InetPathFormat {
deprecated("Use decodeSingleSegment instead.") deprecated("Use decodeSingleSegment instead.")
static auto decodeSegment(S)(string segment) static auto decodeSegment(S)(string segment)
{ {
import std.algorithm.searching : any;
import std.array : array;
import std.exception : assumeUnique;
import std.range : only; import std.range : only;
import std.utf : byCodeUnit;
if (!segment.length) return only(S.fromTrustedString(null)); if (!segment.length) return only(S.fromTrustedString(null));
char sep = '\0'; char sep = '\0';
if (segment[$-1] == '/') { if (segment[$-1] == '/') {
sep = '/'; sep = '/';
segment = segment[0 .. $-1]; segment = segment[0 .. $-1];
} }
return only(S(decodeSingleSegment(segment), sep));
if (!segment.byCodeUnit.any!(c => c == '%'))
return only(S(segment, sep));
string n = decodeSingleSegment(segment).array;
return only(S(n, sep));
} }
unittest { 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); }} 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("foo/").equal([Segment("foo", '/')])); assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
@ -1328,9 +1647,8 @@ struct InetPathFormat {
assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")])); assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")]));
} }
static string decodeSingleSegment(string segment) static auto decodeSingleSegment(string segment)
{ @nogc {
import std.array : appender;
import std.string : indexOf; import std.string : indexOf;
static int hexDigit(char ch) @safe nothrow @nogc { static int hexDigit(char ch) @safe nothrow @nogc {
@ -1340,32 +1658,48 @@ struct InetPathFormat {
else return ch - 'A' + 10; else return ch - 'A' + 10;
} }
static string urlDecode(string s) @safe nothrow { static struct R {
auto idx = s.indexOf('%'); @safe pure nothrow @nogc:
if (idx < 0) return s;
auto ret = appender!string; private {
ret.put(s[0 .. idx]); string m_str;
size_t m_index;
for (size_t i = idx; i < s.length; i++) {
if (s[i] == '%') {
assert(i+2 < 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; this(string s)
{
m_str = s;
} }
assert(segment.length == 0 || segment[$-1] != '/'); @property bool empty() const { return m_index >= m_str.length; }
return urlDecode(segment);
@property char front()
const {
auto ch = m_str[m_index];
if (ch != '%') return ch;
auto a = m_str[m_index+1];
auto b = m_str[m_index+2];
return cast(char)(16 * hexDigit(a) + hexDigit(b));
}
@property void popFront()
{
assert(!empty);
if (m_str[m_index] == '%') m_index += 3;
else m_index++;
}
}
return R(segment);
} }
unittest { unittest {
assert(decodeSingleSegment("foo") == "foo"); scope (failure) assert(false);
assert(decodeSingleSegment("fo%20o\\") == "fo o\\");
assert(decodeSingleSegment("foo%20") == "foo "); assert(decodeSingleSegment("foo").equal("foo"));
assert(decodeSingleSegment("fo%20o\\").equal("fo o\\"));
assert(decodeSingleSegment("foo%20").equal("foo "));
} }
@ -1424,4 +1758,5 @@ struct InetPathFormat {
unittest { // regression tests unittest { // regression tests
assert(NativePath("").bySegment.empty); assert(NativePath("").bySegment.empty);
assert(NativePath("").bySegment2.empty);
} }