1 /**
2 	Contains routines for high level path handling.
3 
4 	Copyright: © 2012 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.internal.vibecompat.inet.path;
9 
10 version (Have_vibe_core) public import vibe.core.path;
11 else:
12 
13 import std.algorithm;
14 import std.array;
15 import std.conv;
16 import std.exception;
17 import std.string;
18 
19 
20 deprecated("Use NativePath instead.")
21 alias Path = NativePath;
22 
23 /**
24 	Represents an absolute or relative file system path.
25 
26 	This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks
27 	are done to disallow invalid operations such as concatenating two absolute paths. It also
28 	validates path strings and allows for easy checking of malicious relative paths.
29 */
30 struct NativePath {
31 	private {
32 		immutable(PathEntry)[] m_nodes;
33 		bool m_absolute = false;
34 		bool m_endsWithSlash = false;
35 	}
36 
37 	alias Segment = PathEntry;
38 
39 	alias bySegment = nodes;
40 
41 	/// Constructs a NativePath object by parsing a path string.
42 	this(string pathstr)
43 	{
44 		m_nodes = splitPath(pathstr);
45 		m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && (m_nodes[0].toString().countUntil(':')>0 || m_nodes[0] == "\\"));
46 		m_endsWithSlash = pathstr.endsWith("/");
47 	}
48 
49 	/// Constructs a path object from a list of PathEntry objects.
50 	this(immutable(PathEntry)[] nodes, bool absolute = false)
51 	{
52 		m_nodes = nodes;
53 		m_absolute = absolute;
54 	}
55 
56 	/// Constructs a relative path with one path entry.
57 	this(PathEntry entry){
58 		m_nodes = [entry];
59 		m_absolute = false;
60 	}
61 
62 	/// Determines if the path is absolute.
63 	@property bool absolute() const scope @safe pure nothrow @nogc { return m_absolute; }
64 
65 	/// Resolves all '.' and '..' path entries as far as possible.
66 	void normalize()
67 	{
68 		immutable(PathEntry)[] newnodes;
69 		foreach( n; m_nodes ){
70 			switch(n.toString()){
71 				default:
72 					newnodes ~= n;
73 					break;
74 				case "", ".": break;
75 				case "..":
76 					enforce(!m_absolute || newnodes.length > 0, "Path goes below root node.");
77 					if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1];
78 					else newnodes ~= n;
79 					break;
80 			}
81 		}
82 		m_nodes = newnodes;
83 	}
84 
85 	/// Converts the Path back to a string representation using slashes.
86 	string toString()
87 	const @safe {
88 		if( m_nodes.empty ) return absolute ? "/" : "";
89 
90 		Appender!string ret;
91 
92 		// for absolute paths start with /
93 		version(Windows)
94 		{
95 			// Make sure windows path isn't "DRIVE:"
96 			if( absolute && !m_nodes[0].toString().endsWith(':') )
97 				ret.put('/');
98 		}
99 		else
100 		{
101 			if( absolute )
102 			{
103 				ret.put('/');
104 			}
105 		}
106 
107 		foreach( i, f; m_nodes ){
108 			if( i > 0 ) ret.put('/');
109 			ret.put(f.toString());
110 		}
111 
112 		if( m_nodes.length > 0 && m_endsWithSlash )
113 			ret.put('/');
114 
115 		return ret.data;
116 	}
117 
118 	/// Converts the NativePath object to a native path string (backslash as path separator on Windows).
119 	string toNativeString()
120 	const {
121 		if (m_nodes.empty) {
122 			version(Windows) {
123 				assert(!absolute, "Empty absolute path detected.");
124 				return m_endsWithSlash ? ".\\" : ".";
125 			} else return absolute ? "/" : m_endsWithSlash ? "./" : ".";
126 		}
127 
128 		Appender!string ret;
129 
130 		// for absolute unix paths start with /
131 		version(Posix) { if(absolute) ret.put('/'); }
132 
133 		foreach( i, f; m_nodes ){
134 			version(Windows) { if( i > 0 ) ret.put('\\'); }
135 			else version(Posix) { if( i > 0 ) ret.put('/'); }
136 			else { static assert(0, "Unsupported OS"); }
137 			ret.put(f.toString());
138 		}
139 
140 		if( m_nodes.length > 0 && m_endsWithSlash ){
141 			version(Windows) { ret.put('\\'); }
142 			version(Posix) { ret.put('/'); }
143 		}
144 
145 		return ret.data;
146 	}
147 
148 	/// Tests if `rhs` is an ancestor or the same as this path.
149 	bool startsWith(const NativePath rhs) const {
150 		if( rhs.m_nodes.length > m_nodes.length ) return false;
151 		foreach( i; 0 .. rhs.m_nodes.length )
152 			if( m_nodes[i] != rhs.m_nodes[i] )
153 				return false;
154 		return true;
155 	}
156 
157 	/// Computes the relative path from `parentPath` to this path.
158 	NativePath relativeTo(const NativePath parentPath) const {
159 		assert(this.absolute && parentPath.absolute, "Determining relative path between non-absolute paths.");
160 		version(Windows){
161 			// a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case
162 			if( this.absolute && !this.empty &&
163 				(m_nodes[0].toString().endsWith(":") && !parentPath.startsWith(this[0 .. 1]) ||
164 				m_nodes[0] == "\\" && !parentPath.startsWith(this[0 .. min(2, $)])))
165 			{
166 				return this;
167 			}
168 		}
169 		int nup = 0;
170 		while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){
171 			nup++;
172 		}
173 		assert(m_nodes.length >= parentPath.length - nup);
174 		NativePath ret = NativePath(null, false);
175 		assert(m_nodes.length >= parentPath.length - nup);
176 		ret.m_endsWithSlash = true;
177 		foreach( i; 0 .. nup ) ret ~= "..";
178 		ret ~= NativePath(m_nodes[parentPath.length-nup .. $], false);
179 		ret.m_endsWithSlash = this.m_endsWithSlash;
180 		return ret;
181 	}
182 
183 	/// The last entry of the path
184 	@property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0, "Getting head of empty path."); return m_nodes[$-1]; }
185 
186 	/// The parent path
187 	@property NativePath parentPath() const { return this[0 .. length-1]; }
188 	/// Forward compatibility with vibe-d
189 	@property bool hasParentPath() const { return length > 1; }
190 
191 	/// The list of path entries of which this path is composed
192 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
193 
194 	/// The number of path entries of which this path is composed
195 	@property size_t length() const scope @safe pure nothrow @nogc { return m_nodes.length; }
196 
197 	/// True if the path contains no entries
198 	@property bool empty() const scope @safe pure nothrow @nogc { return m_nodes.length == 0; }
199 
200 	/// Determines if the path ends with a slash (i.e. is a directory)
201 	@property bool endsWithSlash() const { return m_endsWithSlash; }
202 	/// ditto
203 	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
204 
205 	/// Determines if this path goes outside of its base path (i.e. begins with '..').
206 	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
207 
208 	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
209 	NativePath opSlice(size_t start, size_t end) const {
210 		auto ret = NativePath(m_nodes[start .. end], start == 0 ? absolute : false);
211 		if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash;
212 		return ret;
213 	}
214 	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
215 
216 
217 	NativePath opBinary(string OP)(const NativePath rhs) const if( OP == "~" ) {
218 		NativePath ret;
219 		ret.m_nodes = m_nodes;
220 		ret.m_absolute = m_absolute;
221 		ret.m_endsWithSlash = rhs.m_endsWithSlash;
222 		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
223 
224 		assert(!rhs.absolute, "Trying to append absolute path.");
225 		foreach(folder; rhs.m_nodes){
226 			switch(folder.toString()){
227 				default: ret.m_nodes = ret.m_nodes ~ folder; break;
228 				case "", ".": break;
229 				case "..":
230 					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
231 					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
232 						ret.m_nodes = ret.m_nodes[0 .. $-1];
233 					else ret.m_nodes = ret.m_nodes ~ folder;
234 					break;
235 			}
236 		}
237 		return ret;
238 	}
239 
240 	NativePath opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); }
241 	NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); }
242 	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); }
243 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); }
244 	void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; }
245 
246 	/// Tests two paths for equality using '=='.
247 	bool opEquals(scope ref const NativePath rhs) const scope @safe {
248 		if( m_absolute != rhs.m_absolute ) return false;
249 		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
250 		if( m_nodes.length != rhs.length ) return false;
251 		foreach( i; 0 .. m_nodes.length )
252 			if( m_nodes[i] != rhs.m_nodes[i] )
253 				return false;
254 		return true;
255 	}
256 	/// ditto
257 	bool opEquals(scope const NativePath other) const scope @safe { return opEquals(other); }
258 
259 	int opCmp(ref const NativePath rhs) const {
260 		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
261 		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
262 			if( m_nodes[i] != rhs.m_nodes[i] )
263 				return m_nodes[i].opCmp(rhs.m_nodes[i]);
264 		if( m_nodes.length > rhs.m_nodes.length ) return 1;
265 		if( m_nodes.length < rhs.m_nodes.length ) return -1;
266 		return 0;
267 	}
268 
269 	size_t toHash()
270 	const nothrow @trusted {
271 		size_t ret;
272 		auto strhash = &typeid(string).getHash;
273 		try foreach (n; nodes) ret ^= strhash(&n.m_name);
274 		catch (Exception) assert(false);
275 		if (m_absolute) ret ^= 0xfe3c1738;
276 		if (m_endsWithSlash) ret ^= 0x6aa4352d;
277 		return ret;
278 	}
279 }
280 
281 struct PathEntry {
282 	private {
283 		string m_name;
284 	}
285 
286 	this(string str)
287 	pure {
288 		assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1));
289 		m_name = str;
290 	}
291 
292 	string toString() const return scope @safe pure nothrow @nogc { return m_name; }
293 
294 	@property string name() const return scope @safe pure nothrow @nogc { return m_name; }
295 
296 	NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); }
297 
298 	bool opEquals(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; }
299 	bool opEquals(scope PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; }
300 	bool opEquals(string rhs) const scope @safe pure nothrow @nogc { return m_name == rhs; }
301 	int opCmp(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs.m_name); }
302 	int opCmp(string rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs); }
303 }
304 
305 /// Joins two path strings. sub-path must be relative.
306 string joinPath(string basepath, string subpath)
307 {
308 	NativePath p1 = NativePath(basepath);
309 	NativePath p2 = NativePath(subpath);
310 	return (p1 ~ p2).toString();
311 }
312 
313 /// Splits up a path string into its elements/folders
314 PathEntry[] splitPath(string path)
315 pure {
316 	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
317 	if( path.empty ) return null;
318 	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
319 
320 	// count the number of path nodes
321 	size_t nelements = 0;
322 	foreach( i, char ch; path )
323 		if( ch == '\\' || ch == '/' )
324 			nelements++;
325 	nelements++;
326 
327 	// reserve space for the elements
328 	auto elements = new PathEntry[nelements];
329 	size_t eidx = 0;
330 
331 	// detect UNC path
332 	if(path.startsWith("\\"))
333 	{
334 		elements[eidx++] = PathEntry(path[0 .. 1]);
335 		path = path[1 .. $];
336 	}
337 
338 	// read and return the elements
339 	size_t startidx = 0;
340 	foreach( i, char ch; path )
341 		if( ch == '\\' || ch == '/' ){
342 			elements[eidx++] = PathEntry(path[startidx .. i]);
343 			startidx = i+1;
344 		}
345 	elements[eidx++] = PathEntry(path[startidx .. $]);
346 	assert(eidx == nelements);
347 	return elements;
348 }
349 
350 unittest
351 {
352 	NativePath p;
353 	assert(p.toNativeString() == ".");
354 	p.endsWithSlash = true;
355 	version(Windows) assert(p.toNativeString() == ".\\");
356 	else assert(p.toNativeString() == "./");
357 
358 	p = NativePath("test/");
359 	version(Windows) assert(p.toNativeString() == "test\\");
360 	else assert(p.toNativeString() == "test/");
361 	p.endsWithSlash = false;
362 	assert(p.toNativeString() == "test");
363 }
364 
365 unittest
366 {
367 	{
368 		auto unc = "\\\\server\\share\\path";
369 		auto uncp = NativePath(unc);
370 		uncp.normalize();
371 		version(Windows) assert(uncp.toNativeString() == unc);
372 		assert(uncp.absolute);
373 		assert(!uncp.endsWithSlash);
374 	}
375 
376 	{
377 		auto abspath = "/test/path/";
378 		auto abspathp = NativePath(abspath);
379 		assert(abspathp.toString() == abspath);
380 		version(Windows) {} else assert(abspathp.toNativeString() == abspath);
381 		assert(abspathp.absolute);
382 		assert(abspathp.endsWithSlash);
383 		assert(abspathp.length == 2);
384 		assert(abspathp[0] == "test");
385 		assert(abspathp[1] == "path");
386 	}
387 
388 	{
389 		auto relpath = "test/path/";
390 		auto relpathp = NativePath(relpath);
391 		assert(relpathp.toString() == relpath);
392 		version(Windows) assert(relpathp.toNativeString() == "test\\path\\");
393 		else assert(relpathp.toNativeString() == relpath);
394 		assert(!relpathp.absolute);
395 		assert(relpathp.endsWithSlash);
396 		assert(relpathp.length == 2);
397 		assert(relpathp[0] == "test");
398 		assert(relpathp[1] == "path");
399 	}
400 
401 	{
402 		auto winpath = "C:\\windows\\test";
403 		auto winpathp = NativePath(winpath);
404 		version(Windows) {
405 			assert(winpathp.toString() == "C:/windows/test", winpathp.toString());
406 			assert(winpathp.toNativeString() == winpath);
407 		} else {
408 			assert(winpathp.toString() == "/C:/windows/test", winpathp.toString());
409 			assert(winpathp.toNativeString() == "/C:/windows/test");
410 		}
411 		assert(winpathp.absolute);
412 		assert(!winpathp.endsWithSlash);
413 		assert(winpathp.length == 3);
414 		assert(winpathp[0] == "C:");
415 		assert(winpathp[1] == "windows");
416 		assert(winpathp[2] == "test");
417 	}
418 
419 	{
420 		auto dotpath = "/test/../test2/././x/y";
421 		auto dotpathp = NativePath(dotpath);
422 		assert(dotpathp.toString() == "/test/../test2/././x/y");
423 		dotpathp.normalize();
424 		assert(dotpathp.toString() == "/test2/x/y");
425 	}
426 
427 	{
428 		auto dotpath = "/test/..////test2//./x/y";
429 		auto dotpathp = NativePath(dotpath);
430 		assert(dotpathp.toString() == "/test/..////test2//./x/y");
431 		dotpathp.normalize();
432 		assert(dotpathp.toString() == "/test2/x/y");
433 	}
434 
435 	{
436 		auto parentpath = "/path/to/parent";
437 		auto parentpathp = NativePath(parentpath);
438 		auto subpath = "/path/to/parent/sub/";
439 		auto subpathp = NativePath(subpath);
440 		auto subpath_rel = "sub/";
441 		assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel);
442 		auto subfile = "/path/to/parent/child";
443 		auto subfilep = NativePath(subfile);
444 		auto subfile_rel = "child";
445 		assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel);
446 	}
447 
448 	{ // relative paths across Windows devices are not allowed
449 		version (Windows) {
450 			auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute);
451 			auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute);
452 			auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute);
453 			auto p4 = NativePath("C:\\somepath"); assert(p4.absolute);
454 			auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute);
455 			auto p6 = NativePath("D:\\somepath"); assert(p6.absolute);
456 			assert(p4.relativeTo(p5) == NativePath("../somepath"));
457 			assert(p4.relativeTo(p6) == NativePath("C:\\somepath"));
458 			assert(p4.relativeTo(p1) == NativePath("C:\\somepath"));
459 			assert(p1.relativeTo(p2) == NativePath("../share"));
460 			assert(p1.relativeTo(p3) == NativePath("\\\\server\\share"));
461 			assert(p1.relativeTo(p4) == NativePath("\\\\server\\share"));
462 		}
463 	}
464 }
465 
466 unittest {
467 	assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz");
468 	assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/");
469 	assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar");
470 	assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/");
471 	assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == "..");
472 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../");
473 	assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz");
474 	assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/");
475 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../");
476 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../");
477 	assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == "");
478 	assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == "");
479 }