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)
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 
189 	/// The list of path entries of which this path is composed
190 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
191 
192 	/// The number of path entries of which this path is composed
193 	@property size_t length() const scope @safe pure nothrow @nogc { return m_nodes.length; }
194 
195 	/// True if the path contains no entries
196 	@property bool empty() const scope @safe pure nothrow @nogc { return m_nodes.length == 0; }
197 
198 	/// Determines if the path ends with a slash (i.e. is a directory)
199 	@property bool endsWithSlash() const { return m_endsWithSlash; }
200 	/// ditto
201 	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
202 
203 	/// Determines if this path goes outside of its base path (i.e. begins with '..').
204 	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
205 
206 	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
207 	NativePath opSlice(size_t start, size_t end) const {
208 		auto ret = NativePath(m_nodes[start .. end], start == 0 ? absolute : false);
209 		if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash;
210 		return ret;
211 	}
212 	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
213 
214 
215 	NativePath opBinary(string OP)(const NativePath rhs) const if( OP == "~" ) {
216 		NativePath ret;
217 		ret.m_nodes = m_nodes;
218 		ret.m_absolute = m_absolute;
219 		ret.m_endsWithSlash = rhs.m_endsWithSlash;
220 		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
221 
222 		assert(!rhs.absolute, "Trying to append absolute path.");
223 		foreach(folder; rhs.m_nodes){
224 			switch(folder.toString()){
225 				default: ret.m_nodes = ret.m_nodes ~ folder; break;
226 				case "", ".": break;
227 				case "..":
228 					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
229 					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
230 						ret.m_nodes = ret.m_nodes[0 .. $-1];
231 					else ret.m_nodes = ret.m_nodes ~ folder;
232 					break;
233 			}
234 		}
235 		return ret;
236 	}
237 
238 	NativePath opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); }
239 	NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); }
240 	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); }
241 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); }
242 	void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; }
243 
244 	/// Tests two paths for equality using '=='.
245 	bool opEquals(scope ref const NativePath rhs) const scope @safe {
246 		if( m_absolute != rhs.m_absolute ) return false;
247 		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
248 		if( m_nodes.length != rhs.length ) return false;
249 		foreach( i; 0 .. m_nodes.length )
250 			if( m_nodes[i] != rhs.m_nodes[i] )
251 				return false;
252 		return true;
253 	}
254 	/// ditto
255 	bool opEquals(scope const NativePath other) const scope @safe { return opEquals(other); }
256 
257 	int opCmp(ref const NativePath rhs) const {
258 		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
259 		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
260 			if( m_nodes[i] != rhs.m_nodes[i] )
261 				return m_nodes[i].opCmp(rhs.m_nodes[i]);
262 		if( m_nodes.length > rhs.m_nodes.length ) return 1;
263 		if( m_nodes.length < rhs.m_nodes.length ) return -1;
264 		return 0;
265 	}
266 
267 	size_t toHash()
268 	const nothrow @trusted {
269 		size_t ret;
270 		auto strhash = &typeid(string).getHash;
271 		try foreach (n; nodes) ret ^= strhash(&n.m_name);
272 		catch (Exception) assert(false);
273 		if (m_absolute) ret ^= 0xfe3c1738;
274 		if (m_endsWithSlash) ret ^= 0x6aa4352d;
275 		return ret;
276 	}
277 }
278 
279 struct PathEntry {
280 	private {
281 		string m_name;
282 	}
283 
284 	this(string str)
285 	pure {
286 		assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1));
287 		m_name = str;
288 	}
289 
290 	string toString() const return scope @safe pure nothrow @nogc { return m_name; }
291 
292 	@property string name() const return scope @safe pure nothrow @nogc { return m_name; }
293 
294 	NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); }
295 
296 	bool opEquals(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; }
297 	bool opEquals(scope PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; }
298 	bool opEquals(string rhs) const scope @safe pure nothrow @nogc { return m_name == rhs; }
299 	int opCmp(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs.m_name); }
300 	int opCmp(string rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs); }
301 }
302 
303 /// Joins two path strings. sub-path must be relative.
304 string joinPath(string basepath, string subpath)
305 {
306 	NativePath p1 = NativePath(basepath);
307 	NativePath p2 = NativePath(subpath);
308 	return (p1 ~ p2).toString();
309 }
310 
311 /// Splits up a path string into its elements/folders
312 PathEntry[] splitPath(string path)
313 pure {
314 	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
315 	if( path.empty ) return null;
316 	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
317 
318 	// count the number of path nodes
319 	size_t nelements = 0;
320 	foreach( i, char ch; path )
321 		if( ch == '\\' || ch == '/' )
322 			nelements++;
323 	nelements++;
324 
325 	// reserve space for the elements
326 	auto elements = new PathEntry[nelements];
327 	size_t eidx = 0;
328 
329 	// detect UNC path
330 	if(path.startsWith("\\"))
331 	{
332 		elements[eidx++] = PathEntry(path[0 .. 1]);
333 		path = path[1 .. $];
334 	}
335 
336 	// read and return the elements
337 	size_t startidx = 0;
338 	foreach( i, char ch; path )
339 		if( ch == '\\' || ch == '/' ){
340 			elements[eidx++] = PathEntry(path[startidx .. i]);
341 			startidx = i+1;
342 		}
343 	elements[eidx++] = PathEntry(path[startidx .. $]);
344 	assert(eidx == nelements);
345 	return elements;
346 }
347 
348 unittest
349 {
350 	NativePath p;
351 	assert(p.toNativeString() == ".");
352 	p.endsWithSlash = true;
353 	version(Windows) assert(p.toNativeString() == ".\\");
354 	else assert(p.toNativeString() == "./");
355 
356 	p = NativePath("test/");
357 	version(Windows) assert(p.toNativeString() == "test\\");
358 	else assert(p.toNativeString() == "test/");
359 	p.endsWithSlash = false;
360 	assert(p.toNativeString() == "test");
361 }
362 
363 unittest
364 {
365 	{
366 		auto unc = "\\\\server\\share\\path";
367 		auto uncp = NativePath(unc);
368 		uncp.normalize();
369 		version(Windows) assert(uncp.toNativeString() == unc);
370 		assert(uncp.absolute);
371 		assert(!uncp.endsWithSlash);
372 	}
373 
374 	{
375 		auto abspath = "/test/path/";
376 		auto abspathp = NativePath(abspath);
377 		assert(abspathp.toString() == abspath);
378 		version(Windows) {} else assert(abspathp.toNativeString() == abspath);
379 		assert(abspathp.absolute);
380 		assert(abspathp.endsWithSlash);
381 		assert(abspathp.length == 2);
382 		assert(abspathp[0] == "test");
383 		assert(abspathp[1] == "path");
384 	}
385 
386 	{
387 		auto relpath = "test/path/";
388 		auto relpathp = NativePath(relpath);
389 		assert(relpathp.toString() == relpath);
390 		version(Windows) assert(relpathp.toNativeString() == "test\\path\\");
391 		else assert(relpathp.toNativeString() == relpath);
392 		assert(!relpathp.absolute);
393 		assert(relpathp.endsWithSlash);
394 		assert(relpathp.length == 2);
395 		assert(relpathp[0] == "test");
396 		assert(relpathp[1] == "path");
397 	}
398 
399 	{
400 		auto winpath = "C:\\windows\\test";
401 		auto winpathp = NativePath(winpath);
402 		version(Windows) {
403 			assert(winpathp.toString() == "C:/windows/test", winpathp.toString());
404 			assert(winpathp.toNativeString() == winpath);
405 		} else {
406 			assert(winpathp.toString() == "/C:/windows/test", winpathp.toString());
407 			assert(winpathp.toNativeString() == "/C:/windows/test");
408 		}
409 		assert(winpathp.absolute);
410 		assert(!winpathp.endsWithSlash);
411 		assert(winpathp.length == 3);
412 		assert(winpathp[0] == "C:");
413 		assert(winpathp[1] == "windows");
414 		assert(winpathp[2] == "test");
415 	}
416 
417 	{
418 		auto dotpath = "/test/../test2/././x/y";
419 		auto dotpathp = NativePath(dotpath);
420 		assert(dotpathp.toString() == "/test/../test2/././x/y");
421 		dotpathp.normalize();
422 		assert(dotpathp.toString() == "/test2/x/y");
423 	}
424 
425 	{
426 		auto dotpath = "/test/..////test2//./x/y";
427 		auto dotpathp = NativePath(dotpath);
428 		assert(dotpathp.toString() == "/test/..////test2//./x/y");
429 		dotpathp.normalize();
430 		assert(dotpathp.toString() == "/test2/x/y");
431 	}
432 
433 	{
434 		auto parentpath = "/path/to/parent";
435 		auto parentpathp = NativePath(parentpath);
436 		auto subpath = "/path/to/parent/sub/";
437 		auto subpathp = NativePath(subpath);
438 		auto subpath_rel = "sub/";
439 		assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel);
440 		auto subfile = "/path/to/parent/child";
441 		auto subfilep = NativePath(subfile);
442 		auto subfile_rel = "child";
443 		assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel);
444 	}
445 
446 	{ // relative paths across Windows devices are not allowed
447 		version (Windows) {
448 			auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute);
449 			auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute);
450 			auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute);
451 			auto p4 = NativePath("C:\\somepath"); assert(p4.absolute);
452 			auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute);
453 			auto p6 = NativePath("D:\\somepath"); assert(p6.absolute);
454 			assert(p4.relativeTo(p5) == NativePath("../somepath"));
455 			assert(p4.relativeTo(p6) == NativePath("C:\\somepath"));
456 			assert(p4.relativeTo(p1) == NativePath("C:\\somepath"));
457 			assert(p1.relativeTo(p2) == NativePath("../share"));
458 			assert(p1.relativeTo(p3) == NativePath("\\\\server\\share"));
459 			assert(p1.relativeTo(p4) == NativePath("\\\\server\\share"));
460 		}
461 	}
462 }
463 
464 unittest {
465 	assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz");
466 	assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/");
467 	assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar");
468 	assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/");
469 	assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == "..");
470 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../");
471 	assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz");
472 	assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/");
473 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../");
474 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../");
475 	assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == "");
476 	assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == "");
477 }