Merge pull request #179 from vibe-d/path_optimization
Add a nogc path segment API
This commit is contained in:
commit
24a83434e4
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
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.
|
||||
Authors: Sönke Ludwig
|
||||
*/
|
||||
module vibe.core.path;
|
||||
|
||||
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.exception : enforce;
|
||||
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
|
||||
if (isInstanceOf!(GenericPath, Path))
|
||||
{
|
||||
import std.algorithm.comparison : equal;
|
||||
import std.array : array, replicate;
|
||||
import std.range : chain, drop, take;
|
||||
|
||||
|
@ -37,35 +36,35 @@ Path relativeTo(Path)(Path path, Path base_path) @safe
|
|||
if (is(Path == WindowsPath)) { // FIXME: this shouldn't be a special case here!
|
||||
bool samePrefix(size_t n)
|
||||
{
|
||||
return path.bySegment.map!(n => n.name).take(n).equal(base_path.bySegment.map!(n => n.name).take(n));
|
||||
return path.bySegment2.map!(n => n.encodedName).take(n).equal(base_path.bySegment2.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.name == "") {
|
||||
auto pref = path.bySegment2;
|
||||
if (!pref.empty && pref.front.encodedName == "") {
|
||||
pref.popFront();
|
||||
if (!pref.empty) {
|
||||
// different drive?
|
||||
if (pref.front.name.endsWith(':') && !samePrefix(2))
|
||||
if (pref.front.encodedName.endsWith(':') && !samePrefix(2))
|
||||
return path;
|
||||
// different UNC path?
|
||||
if (pref.front.name == "" && !samePrefix(4))
|
||||
if (pref.front.encodedName == "" && !samePrefix(4))
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto nodes = path.bySegment;
|
||||
auto base_nodes = base_path.bySegment;
|
||||
auto nodes = path.bySegment2;
|
||||
auto base_nodes = base_path.bySegment2;
|
||||
|
||||
// skip and count common prefix
|
||||
size_t base = 0;
|
||||
while (!nodes.empty && !base_nodes.empty && nodes.front.name == base_nodes.front.name) {
|
||||
while (!nodes.empty && !base_nodes.empty && equal(nodes.front.name, base_nodes.front.name)) {
|
||||
nodes.popFront();
|
||||
base_nodes.popFront();
|
||||
base++;
|
||||
}
|
||||
|
||||
enum up = Path.Segment("..", Path.defaultSeparator);
|
||||
enum up = Path.Segment2("..", Path.defaultSeparator);
|
||||
auto ret = Path(base_nodes.map!(p => up).chain(nodes));
|
||||
if (path.endsWithSlash) {
|
||||
if (ret.empty) return Path("." ~ path.toString()[$-1]);
|
||||
|
@ -214,7 +213,6 @@ struct GenericPath(F) {
|
|||
|
||||
private {
|
||||
string m_name;
|
||||
string m_encodedName;
|
||||
char m_separator = 0;
|
||||
}
|
||||
|
||||
|
@ -319,7 +317,7 @@ struct GenericPath(F) {
|
|||
|
||||
private {
|
||||
string m_path;
|
||||
ReturnType!(Format.decodeSegment!Segment) m_fronts;
|
||||
Segment m_front;
|
||||
}
|
||||
|
||||
private this(string path)
|
||||
|
@ -327,37 +325,236 @@ struct GenericPath(F) {
|
|||
m_path = path;
|
||||
if (m_path.length) {
|
||||
auto ap = Format.getAbsolutePrefix(m_path);
|
||||
if (ap.length) {
|
||||
m_fronts = Format.decodeSegment!Segment(ap);
|
||||
m_path = m_path[ap.length .. $];
|
||||
assert(!m_fronts.empty);
|
||||
} else readFront();
|
||||
} else assert(m_fronts.empty);
|
||||
if (ap.length && !Format.isSeparator(ap[0]))
|
||||
m_front = Segment.fromTrustedString(null, '/');
|
||||
else readFront();
|
||||
}
|
||||
}
|
||||
|
||||
@property bool empty() const nothrow @nogc { return m_path.length == 0 && m_fronts.empty; }
|
||||
@property bool empty() const nothrow @nogc { return m_path.length == 0 && m_front == Segment.init; }
|
||||
|
||||
@property PathRange save() { return this; }
|
||||
|
||||
@property Segment front() { return m_fronts.front; }
|
||||
@property Segment front() { return m_front; }
|
||||
|
||||
void popFront()
|
||||
nothrow {
|
||||
assert(!m_fronts.empty);
|
||||
m_fronts.popFront();
|
||||
if (m_fronts.empty && m_path.length)
|
||||
readFront();
|
||||
assert(m_front != Segment.init);
|
||||
if (m_path.length) readFront();
|
||||
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()
|
||||
{
|
||||
auto n = Format.getFrontNode(m_path);
|
||||
m_fronts = Format.decodeSegment!Segment(n);
|
||||
m_path = m_path[n.length .. $];
|
||||
assert(!m_fronts.empty);
|
||||
|
||||
char sep = '\0';
|
||||
if (Format.isSeparator(n[$-1])) {
|
||||
sep = n[$-1];
|
||||
n = n[0 .. $-1];
|
||||
}
|
||||
m_front = Segment2.fromTrustedEncodedString(n, sep);
|
||||
assert(m_front != Segment2.init);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private {
|
||||
string m_path;
|
||||
}
|
||||
|
@ -388,6 +585,12 @@ struct GenericPath(F) {
|
|||
import std.range : only;
|
||||
this(only(segment));
|
||||
}
|
||||
/// ditto
|
||||
this(Segment2 segment)
|
||||
{
|
||||
import std.range : only;
|
||||
this(only(segment));
|
||||
}
|
||||
|
||||
/** Constructs a path from an input range of `Segment`s.
|
||||
|
||||
|
@ -403,6 +606,15 @@ struct GenericPath(F) {
|
|||
Format.toString(segments, dst);
|
||||
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.
|
||||
|
||||
|
@ -449,16 +661,39 @@ struct GenericPath(F) {
|
|||
/// Iterates over the path by `Segment`.
|
||||
@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.
|
||||
@property Segment head()
|
||||
const {
|
||||
auto s = Format.decodeSegment!Segment(Format.getBackNode(m_path));
|
||||
auto ret = s.front;
|
||||
while (!s.empty) {
|
||||
s.popFront();
|
||||
if (!s.empty) ret = s.front;
|
||||
import std.array : array;
|
||||
|
||||
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 ret;
|
||||
|
||||
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.
|
||||
|
@ -503,20 +738,20 @@ struct GenericPath(F) {
|
|||
{
|
||||
import std.array : appender, join;
|
||||
|
||||
Segment[] newnodes;
|
||||
Segment2[] newnodes;
|
||||
bool got_non_sep = false;
|
||||
foreach (n; this.bySegment) {
|
||||
foreach (n; this.bySegment2) {
|
||||
if (n.hasSeparator) n.separator = Format.defaultSeparator;
|
||||
if (!got_non_sep) {
|
||||
if (n.name == "") newnodes ~= n;
|
||||
if (n.encodedName == "") newnodes ~= n;
|
||||
else got_non_sep = true;
|
||||
}
|
||||
switch (n.name) {
|
||||
switch (n.encodedName) {
|
||||
default: newnodes ~= n; break;
|
||||
case "", ".": break;
|
||||
case "..":
|
||||
enforce(!this.absolute || newnodes.length > 0, "Path goes below root node.");
|
||||
if (newnodes.length > 0 && newnodes[$-1].name != "..") newnodes = newnodes[0 .. $-1];
|
||||
if (newnodes.length > 0 && newnodes[$-1].encodedName != "..") newnodes = newnodes[0 .. $-1];
|
||||
else newnodes ~= n;
|
||||
break;
|
||||
}
|
||||
|
@ -567,7 +802,7 @@ struct GenericPath(F) {
|
|||
*/
|
||||
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));
|
||||
else return P(this.bySegment2.map!(n => cast(P.Segment2)n));
|
||||
}
|
||||
|
||||
/** Concatenates two paths.
|
||||
|
@ -578,8 +813,12 @@ struct GenericPath(F) {
|
|||
/// ditto
|
||||
GenericPath opBinary(string op : "~")(Segment subpath) const { return this ~ GenericPath(subpath); }
|
||||
/// 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); }
|
||||
/// 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 {
|
||||
assert(!subpath.absolute || m_path.length == 0, "Cannot append absolute path.");
|
||||
if (endsWithSlash || empty) return GenericPath.fromTrustedString(m_path ~ subpath.m_path);
|
||||
|
@ -593,6 +832,12 @@ struct GenericPath(F) {
|
|||
{
|
||||
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.
|
||||
void opOpAssign(string op : "~", T)(T op) { this = this ~ op; }
|
||||
|
@ -603,13 +848,11 @@ struct GenericPath(F) {
|
|||
*/
|
||||
bool startsWith(GenericPath prefix)
|
||||
const nothrow {
|
||||
return bySegment.map!(n => n.name).startsWith(prefix.bySegment.map!(n => n.name));
|
||||
return bySegment2.map!(n => n.name).startsWith(prefix.bySegment2.map!(n => n.name));
|
||||
}
|
||||
}
|
||||
|
||||
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("",'/'), PosixPath.Segment("hello",'/'), PosixPath.Segment("world",'/')]));
|
||||
assert(PosixPath("hello\\world").bySegment.equal([PosixPath.Segment("hello\\world")]));
|
||||
|
@ -619,10 +862,18 @@ unittest {
|
|||
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
|
||||
{
|
||||
import std.algorithm.comparison : equal;
|
||||
|
||||
{
|
||||
auto unc = "\\\\server\\share\\path";
|
||||
auto uncp = WindowsPath(unc);
|
||||
|
@ -702,6 +953,55 @@ unittest
|
|||
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 {
|
||||
import std.array : appender;
|
||||
auto app = appender!(PosixPath[]);
|
||||
|
@ -712,7 +1012,6 @@ unittest
|
|||
}
|
||||
|
||||
unittest {
|
||||
import std.algorithm.comparison : equal;
|
||||
import std.exception : assertThrown, assertNotThrown;
|
||||
|
||||
assertThrown!PathValidationException(WindowsPath.Segment("foo/bar"));
|
||||
|
@ -725,6 +1024,20 @@ unittest {
|
|||
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 {
|
||||
assert(!PosixPath("").hasParentPath);
|
||||
assert(!PosixPath("/").hasParentPath);
|
||||
|
@ -752,6 +1065,10 @@ unittest {
|
|||
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.
|
||||
class PathValidationException : Exception {
|
||||
this(string text, string file = __FILE__, size_t line = cast(size_t)__LINE__, Throwable next = null)
|
||||
|
@ -893,6 +1210,7 @@ struct WindowsPathFormat {
|
|||
assert(getBackNode("foo\\") == "foo\\");
|
||||
}
|
||||
|
||||
deprecated("Use decodeSingleSegment instead.")
|
||||
static auto decodeSegment(S)(string segment)
|
||||
{
|
||||
static struct R {
|
||||
|
@ -922,7 +1240,6 @@ struct WindowsPathFormat {
|
|||
}
|
||||
|
||||
unittest {
|
||||
import std.algorithm.comparison : equal;
|
||||
struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }}
|
||||
assert(decodeSegment!Segment("foo").equal([Segment("foo")]));
|
||||
assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
|
||||
|
@ -931,6 +1248,20 @@ struct WindowsPathFormat {
|
|||
assert(decodeSegment!Segment("bar:\\").equal([Segment("",'/'), Segment("bar:", '\\')]));
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -981,6 +1312,12 @@ struct WindowsPathFormat {
|
|||
assert(validatePath("foo\\b:ar") !is null);
|
||||
assert(validatePath("foo\\bar:\\baz") !is null);
|
||||
}
|
||||
|
||||
static string encodeSegment(string segment)
|
||||
{
|
||||
assert(segment.length == 0 || segment[$-1] != '/');
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1095,6 +1432,7 @@ struct PosixPathFormat {
|
|||
assert(validatePath("foo\0bar") !is null);
|
||||
}
|
||||
|
||||
deprecated("Use decodeSingleSegment instead.")
|
||||
static auto decodeSegment(S)(string segment)
|
||||
{
|
||||
assert(segment.length > 0, "Path segment string must not be empty.");
|
||||
|
@ -1106,12 +1444,29 @@ struct PosixPathFormat {
|
|||
}
|
||||
|
||||
unittest {
|
||||
import std.algorithm.comparison : equal;
|
||||
struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }}
|
||||
assert(decodeSegment!Segment("foo").equal([Segment("foo")]));
|
||||
assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
|
||||
assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo%20o\\")]));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1122,28 +1477,14 @@ struct PosixPathFormat {
|
|||
struct InetPathFormat {
|
||||
static void toString(I, O)(I segments, O dst)
|
||||
{
|
||||
import std.format : formattedWrite;
|
||||
|
||||
char lastsep = '\0';
|
||||
bool first = true;
|
||||
foreach (e; segments) {
|
||||
if (!first || lastsep) dst.put('/');
|
||||
else first = false;
|
||||
foreach (char c; e.name) {
|
||||
switch (c) {
|
||||
default:
|
||||
dst.formattedWrite("%%%02X", c);
|
||||
break;
|
||||
case 'a': .. case 'z':
|
||||
case 'A': .. case 'Z':
|
||||
case '0': .. case '9':
|
||||
case '-', '.', '_', '~':
|
||||
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=':
|
||||
case ':', '@':
|
||||
dst.put(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
static if (is(typeof(e.encodedName)))
|
||||
dst.put(e.encodedName);
|
||||
else encodeSegment(dst, e.name);
|
||||
lastsep = e.separator;
|
||||
}
|
||||
if (lastsep) dst.put('/');
|
||||
|
@ -1203,10 +1544,13 @@ struct InetPathFormat {
|
|||
|
||||
static string getBackNode(string path)
|
||||
@nogc {
|
||||
import std.string : lastIndexOf;
|
||||
|
||||
if (!path.length) return path;
|
||||
foreach_reverse (i; 0 .. path.length-1)
|
||||
if (path[i] == '/')
|
||||
return path[i+1 .. $];
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1273,11 +1617,38 @@ struct InetPathFormat {
|
|||
assert(validatePath("föö") !is null);
|
||||
}
|
||||
|
||||
deprecated("Use decodeSingleSegment instead.")
|
||||
static auto decodeSegment(S)(string segment)
|
||||
{
|
||||
import std.array : appender;
|
||||
import std.format : formattedRead;
|
||||
import std.algorithm.searching : any;
|
||||
import std.array : array;
|
||||
import std.exception : assumeUnique;
|
||||
import std.range : only;
|
||||
import std.utf : byCodeUnit;
|
||||
|
||||
if (!segment.length) return only(S.fromTrustedString(null));
|
||||
char sep = '\0';
|
||||
if (segment[$-1] == '/') {
|
||||
sep = '/';
|
||||
segment = segment[0 .. $-1];
|
||||
}
|
||||
|
||||
if (!segment.byCodeUnit.any!(c => c == '%'))
|
||||
return only(S(segment, sep));
|
||||
string n = decodeSingleSegment(segment).array;
|
||||
return only(S(n, sep));
|
||||
}
|
||||
|
||||
unittest {
|
||||
struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }}
|
||||
assert(decodeSegment!Segment("foo").equal([Segment("foo")]));
|
||||
assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
|
||||
assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo o\\")]));
|
||||
assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")]));
|
||||
}
|
||||
|
||||
static auto decodeSingleSegment(string segment)
|
||||
@nogc {
|
||||
import std.string : indexOf;
|
||||
|
||||
static int hexDigit(char ch) @safe nothrow @nogc {
|
||||
|
@ -1287,40 +1658,105 @@ struct InetPathFormat {
|
|||
else return ch - 'A' + 10;
|
||||
}
|
||||
|
||||
static string urlDecode(string s) @safe nothrow {
|
||||
auto idx = s.indexOf('%');
|
||||
if (idx < 0) return s;
|
||||
static struct R {
|
||||
@safe pure nothrow @nogc:
|
||||
|
||||
auto ret = appender!string;
|
||||
ret.put(s[0 .. idx]);
|
||||
|
||||
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]);
|
||||
private {
|
||||
string m_str;
|
||||
size_t m_index;
|
||||
}
|
||||
|
||||
return ret.data;
|
||||
this(string s)
|
||||
{
|
||||
m_str = s;
|
||||
}
|
||||
|
||||
@property bool empty() const { return m_index >= m_str.length; }
|
||||
|
||||
@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++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!segment.length) return only(S.fromTrustedString(null));
|
||||
if (segment[$-1] == '/')
|
||||
return only(S.fromTrustedString(urlDecode(segment[0 .. $-1]), '/'));
|
||||
return only(S.fromTrustedString(urlDecode(segment)));
|
||||
return R(segment);
|
||||
}
|
||||
|
||||
unittest {
|
||||
import std.algorithm.comparison : equal;
|
||||
struct Segment { string name; char separator = 0; static Segment fromTrustedString(string str, char sep = 0) pure nothrow @nogc { return Segment(str, sep); }}
|
||||
assert(decodeSegment!Segment("foo").equal([Segment("foo")]));
|
||||
assert(decodeSegment!Segment("foo/").equal([Segment("foo", '/')]));
|
||||
assert(decodeSegment!Segment("fo%20o\\").equal([Segment("fo o\\")]));
|
||||
assert(decodeSegment!Segment("foo%20").equal([Segment("foo ")]));
|
||||
scope (failure) assert(false);
|
||||
|
||||
assert(decodeSingleSegment("foo").equal("foo"));
|
||||
assert(decodeSingleSegment("fo%20o\\").equal("fo o\\"));
|
||||
assert(decodeSingleSegment("foo%20").equal("foo "));
|
||||
}
|
||||
|
||||
|
||||
static string encodeSegment(string segment)
|
||||
{
|
||||
import std.array : appender;
|
||||
|
||||
foreach (i, char c; segment) {
|
||||
switch (c) {
|
||||
default:
|
||||
auto ret = appender!string;
|
||||
ret.put(segment[0 .. i]);
|
||||
encodeSegment(ret, segment[i .. $]);
|
||||
return ret.data;
|
||||
case 'a': .. case 'z':
|
||||
case 'A': .. case 'Z':
|
||||
case '0': .. case '9':
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unittest { // regression tests
|
||||
assert(NativePath("").bySegment.empty);
|
||||
assert(NativePath("").bySegment2.empty);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue