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