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_d) public import vibe.inet.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 /**
21 	Represents an absolute or relative file system path.
22 
23 	This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks
24 	are done to disallow invalid operations such as concatenating two absolute paths. It also
25 	validates path strings and allows for easy checking of malicious relative paths.
26 */
27 struct Path {
28 	private {
29 		immutable(PathEntry)[] m_nodes;
30 		bool m_absolute = false;
31 		bool m_endsWithSlash = false;
32 	}
33 
34 	/// Constructs a Path object by parsing a path string.
35 	this(string pathstr)
36 	{
37 		static if (__VERSION__ < 2066) m_nodes = splitPath(pathstr).idup;
38 		else m_nodes = splitPath(pathstr);
39 		m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && (m_nodes[0].toString().countUntil(':')>0 || m_nodes[0] == "\\"));
40 		m_endsWithSlash = pathstr.endsWith("/");
41 	}
42 
43 	/// Constructs a path object from a list of PathEntry objects.
44 	this(immutable(PathEntry)[] nodes, bool absolute)
45 	{
46 		m_nodes = nodes;
47 		m_absolute = absolute;
48 	}
49 
50 	/// Constructs a relative path with one path entry.
51 	this(PathEntry entry){
52 		m_nodes = [entry];
53 		m_absolute = false;
54 	}
55 
56 	/// Determines if the path is absolute.
57 	@property bool absolute() const { return m_absolute; }
58 
59 	/// Resolves all '.' and '..' path entries as far as possible.
60 	void normalize()
61 	{
62 		immutable(PathEntry)[] newnodes;
63 		foreach( n; m_nodes ){
64 			switch(n.toString()){
65 				default:
66 					newnodes ~= n;
67 					break;
68 				case "", ".": break;
69 				case "..":
70 					enforce(!m_absolute || newnodes.length > 0, "Path goes below root node.");
71 					if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1];
72 					else newnodes ~= n;
73 					break;
74 			}
75 		}
76 		m_nodes = newnodes;
77 	}
78 
79 	/// Converts the Path back to a string representation using slashes.
80 	string toString()
81 	const {
82 		if( m_nodes.empty ) return absolute ? "/" : "";
83 
84 		Appender!string ret;
85 
86 		// for absolute paths start with /
87 		version(Windows)
88 		{
89 			// Make sure windows path isn't "DRIVE:"
90 			if( absolute && !m_nodes[0].toString().endsWith(':') )
91 				ret.put('/');
92 		}
93 		else
94 		{
95 			if( absolute )
96 			{
97 				ret.put('/');
98 			}
99 		}
100 
101 		foreach( i, f; m_nodes ){
102 			if( i > 0 ) ret.put('/');
103 			ret.put(f.toString());
104 		}
105 
106 		if( m_nodes.length > 0 && m_endsWithSlash )
107 			ret.put('/');
108 
109 		return ret.data;
110 	}
111 
112 	/// Converts the Path object to a native path string (backslash as path separator on Windows).
113 	string toNativeString()
114 	const {
115 		if (m_nodes.empty) {
116 			version(Windows) {
117 				assert(!absolute, "Empty absolute path detected.");
118 				return m_endsWithSlash ? ".\\" : ".";
119 			} else return absolute ? "/" : m_endsWithSlash ? "./" : ".";
120 		}
121 
122 		Appender!string ret;
123 
124 		// for absolute unix paths start with /
125 		version(Posix) { if(absolute) ret.put('/'); }
126 
127 		foreach( i, f; m_nodes ){
128 			version(Windows) { if( i > 0 ) ret.put('\\'); }
129 			version(Posix) { if( i > 0 ) ret.put('/'); }
130 			else { enforce("Unsupported OS"); }
131 			ret.put(f.toString());
132 		}
133 
134 		if( m_nodes.length > 0 && m_endsWithSlash ){
135 			version(Windows) { ret.put('\\'); }
136 			version(Posix) { ret.put('/'); }
137 		}
138 
139 		return ret.data;
140 	}
141 
142 	/// Tests if `rhs` is an anchestor or the same as this path.
143 	bool startsWith(const Path rhs) const {
144 		if( rhs.m_nodes.length > m_nodes.length ) return false;
145 		foreach( i; 0 .. rhs.m_nodes.length )
146 			if( m_nodes[i] != rhs.m_nodes[i] )
147 				return false;
148 		return true;
149 	}
150 
151 	/// Computes the relative path from `parentPath` to this path.
152 	Path relativeTo(const Path parentPath) const {
153 		assert(this.absolute && parentPath.absolute, "Determining relative path between non-absolute paths.");
154 		version(Windows){
155 			// a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case
156 			if( this.absolute && !this.empty &&
157 				(m_nodes[0].toString().endsWith(":") && !parentPath.startsWith(this[0 .. 1]) ||
158 				m_nodes[0] == "\\" && !parentPath.startsWith(this[0 .. min(2, $)])))
159 			{
160 				return this;
161 			}
162 		}
163 		int nup = 0;
164 		while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){
165 			nup++;
166 		}
167 		assert(m_nodes.length >= parentPath.length - nup);
168 		Path ret = Path(null, false);
169 		assert(m_nodes.length >= parentPath.length - nup);
170 		ret.m_endsWithSlash = true;
171 		foreach( i; 0 .. nup ) ret ~= "..";
172 		ret ~= Path(m_nodes[parentPath.length-nup .. $], false);
173 		ret.m_endsWithSlash = this.m_endsWithSlash;
174 		return ret;
175 	}
176 
177 	/// The last entry of the path
178 	@property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; }
179 
180 	/// The parent path
181 	@property Path parentPath() const { return this[0 .. length-1]; }
182 
183 	/// The ist of path entries of which this path is composed
184 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
185 
186 	/// The number of path entries of which this path is composed
187 	@property size_t length() const { return m_nodes.length; }
188 
189 	/// True if the path contains no entries
190 	@property bool empty() const { return m_nodes.length == 0; }
191 
192 	/// Determines if the path ends with a slash (i.e. is a directory)
193 	@property bool endsWithSlash() const { return m_endsWithSlash; }
194 	/// ditto
195 	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
196 
197 	/// Determines if this path goes outside of its base path (i.e. begins with '..').
198 	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
199 
200 	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
201 	Path opSlice(size_t start, size_t end) const {
202 		auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false);
203 		if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash;
204 		return ret;
205 	}
206 	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
207 
208 
209 	Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) {
210 		Path ret;
211 		ret.m_nodes = m_nodes;
212 		ret.m_absolute = m_absolute;
213 		ret.m_endsWithSlash = rhs.m_endsWithSlash;
214 		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
215 
216 		assert(!rhs.absolute, "Trying to append absolute path.");
217 		size_t idx = m_nodes.length;
218 		foreach(folder; rhs.m_nodes){
219 			switch(folder.toString()){
220 				default: ret.m_nodes = ret.m_nodes ~ folder; break;
221 				case "", ".": break;
222 				case "..":
223 					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
224 					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
225 						ret.m_nodes = ret.m_nodes[0 .. $-1];
226 					else ret.m_nodes = ret.m_nodes ~ folder;
227 					break;
228 			}
229 		}
230 		return ret;
231 	}
232 
233 	Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(Path(rhs)); }
234 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(Path(rhs)); }
235 	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(Path(rhs)); }
236 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(Path(rhs)); }
237 	void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; }
238 
239 	/// Tests two paths for equality using '=='.
240 	bool opEquals(ref const Path rhs) const {
241 		if( m_absolute != rhs.m_absolute ) return false;
242 		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
243 		if( m_nodes.length != rhs.length ) return false;
244 		foreach( i; 0 .. m_nodes.length )
245 			if( m_nodes[i] != rhs.m_nodes[i] )
246 				return false;
247 		return true;
248 	}
249 	/// ditto
250 	bool opEquals(const Path other) const { return opEquals(other); }
251 
252 	int opCmp(ref const Path rhs) const {
253 		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
254 		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
255 			if( m_nodes[i] != rhs.m_nodes[i] )
256 				return m_nodes[i].opCmp(rhs.m_nodes[i]);
257 		if( m_nodes.length > rhs.m_nodes.length ) return 1;
258 		if( m_nodes.length < rhs.m_nodes.length ) return -1;
259 		return 0;
260 	}
261 
262 	hash_t toHash()
263 	const nothrow @trusted {
264 		hash_t ret;
265 		auto strhash = &typeid(string).getHash;
266 		try foreach (n; nodes) ret ^= strhash(&n.m_name);
267 		catch (Exception) assert(false);
268 		if (m_absolute) ret ^= 0xfe3c1738;
269 		if (m_endsWithSlash) ret ^= 0x6aa4352d;
270 		return ret;
271 	}
272 }
273 
274 struct PathEntry {
275 	private {
276 		string m_name;
277 	}
278 
279 	this(string str)
280 	pure {
281 		assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1));
282 		m_name = str;
283 	}
284 
285 	string toString() const pure { return m_name; }
286 
287 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path([this, rhs], false); }
288 
289 	bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; }
290 	bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; }
291 	bool opEquals(string rhs) const { return m_name == rhs; }
292 	int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); }
293 	int opCmp(string rhs) const { return m_name.cmp(rhs); }
294 }
295 
296 private bool isValidFilename(string str)
297 {
298 	foreach( ch; str )
299 		if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false;
300 	return true;
301 }
302 
303 /// Joins two path strings. subpath must be relative.
304 string joinPath(string basepath, string subpath)
305 {
306 	Path p1 = Path(basepath);
307 	Path p2 = Path(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 	Path p;
351 	assert(p.toNativeString() == ".");
352 	p.endsWithSlash = true;
353 	version(Windows) assert(p.toNativeString() == ".\\");
354 	else assert(p.toNativeString() == "./");
355 
356 	p = Path("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 = Path(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 = Path(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 = Path(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 = Path(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 = Path(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 = Path(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 = Path(parentpath);
436 		auto subpath = "/path/to/parent/sub/";
437 		auto subpathp = Path(subpath);
438 		auto subpath_rel = "sub/";
439 		assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel);
440 		auto subfile = "/path/to/parent/child";
441 		auto subfilep = Path(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 = Path("\\\\server\\share"); assert(p1.absolute);
449 			auto p2 = Path("\\\\server\\othershare"); assert(p2.absolute);
450 			auto p3 = Path("\\\\otherserver\\share"); assert(p3.absolute);
451 			auto p4 = Path("C:\\somepath"); assert(p4.absolute);
452 			auto p5 = Path("C:\\someotherpath"); assert(p5.absolute);
453 			auto p6 = Path("D:\\somepath"); assert(p6.absolute);
454 			assert(p4.relativeTo(p5) == Path("../somepath"));
455 			assert(p4.relativeTo(p6) == Path("C:\\somepath"));
456 			assert(p4.relativeTo(p1) == Path("C:\\somepath"));
457 			assert(p1.relativeTo(p2) == Path("../share"));
458 			assert(p1.relativeTo(p3) == Path("\\\\server\\share"));
459 			assert(p1.relativeTo(p4) == Path("\\\\server\\share"));
460 		}
461 	}
462 }
463 
464 unittest {
465 	assert(Path("/foo/bar/baz").relativeTo(Path("/foo")).toString == "bar/baz");
466 	assert(Path("/foo/bar/baz/").relativeTo(Path("/foo")).toString == "bar/baz/");
467 	assert(Path("/foo/bar").relativeTo(Path("/foo")).toString == "bar");
468 	assert(Path("/foo/bar/").relativeTo(Path("/foo")).toString == "bar/");
469 	assert(Path("/foo").relativeTo(Path("/foo/bar")).toString() == "..");
470 	assert(Path("/foo/").relativeTo(Path("/foo/bar")).toString() == "../");
471 	assert(Path("/foo/baz").relativeTo(Path("/foo/bar/baz")).toString() == "../../baz");
472 	assert(Path("/foo/baz/").relativeTo(Path("/foo/bar/baz")).toString() == "../../baz/");
473 	assert(Path("/foo/").relativeTo(Path("/foo/bar/baz")).toString() == "../../");
474 	assert(Path("/foo/").relativeTo(Path("/foo/bar/baz/mumpitz")).toString() == "../../../");
475 	assert(Path("/foo").relativeTo(Path("/foo")).toString() == "");
476 	assert(Path("/foo/").relativeTo(Path("/foo")).toString() == "");
477 }