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 		size_t idx = m_nodes.length;
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(ref const NativePath rhs) const {
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(const NativePath other) const { 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 	hash_t toHash()
270 	const nothrow @trusted {
271 		hash_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 pure { return m_name; }
293 
294 	@property string name() const { return m_name; }
295 
296 	NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); }
297 
298 	bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; }
299 	bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; }
300 	bool opEquals(string rhs) const { return m_name == rhs; }
301 	int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); }
302 	int opCmp(string rhs) const { return m_name.cmp(rhs); }
303 }
304 
305 private bool isValidFilename(string str)
306 {
307 	foreach( ch; str )
308 		if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false;
309 	return true;
310 }
311 
312 /// Joins two path strings. subpath must be relative.
313 string joinPath(string basepath, string subpath)
314 {
315 	NativePath p1 = NativePath(basepath);
316 	NativePath p2 = NativePath(subpath);
317 	return (p1 ~ p2).toString();
318 }
319 
320 /// Splits up a path string into its elements/folders
321 PathEntry[] splitPath(string path)
322 pure {
323 	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
324 	if( path.empty ) return null;
325 	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
326 
327 	// count the number of path nodes
328 	size_t nelements = 0;
329 	foreach( i, char ch; path )
330 		if( ch == '\\' || ch == '/' )
331 			nelements++;
332 	nelements++;
333 
334 	// reserve space for the elements
335 	auto elements = new PathEntry[nelements];
336 	size_t eidx = 0;
337 
338 	// detect UNC path
339 	if(path.startsWith("\\"))
340 	{
341 		elements[eidx++] = PathEntry(path[0 .. 1]);
342 		path = path[1 .. $];
343 	}
344 
345 	// read and return the elements
346 	size_t startidx = 0;
347 	foreach( i, char ch; path )
348 		if( ch == '\\' || ch == '/' ){
349 			elements[eidx++] = PathEntry(path[startidx .. i]);
350 			startidx = i+1;
351 		}
352 	elements[eidx++] = PathEntry(path[startidx .. $]);
353 	assert(eidx == nelements);
354 	return elements;
355 }
356 
357 unittest
358 {
359 	NativePath p;
360 	assert(p.toNativeString() == ".");
361 	p.endsWithSlash = true;
362 	version(Windows) assert(p.toNativeString() == ".\\");
363 	else assert(p.toNativeString() == "./");
364 
365 	p = NativePath("test/");
366 	version(Windows) assert(p.toNativeString() == "test\\");
367 	else assert(p.toNativeString() == "test/");
368 	p.endsWithSlash = false;
369 	assert(p.toNativeString() == "test");
370 }
371 
372 unittest
373 {
374 	{
375 		auto unc = "\\\\server\\share\\path";
376 		auto uncp = NativePath(unc);
377 		uncp.normalize();
378 		version(Windows) assert(uncp.toNativeString() == unc);
379 		assert(uncp.absolute);
380 		assert(!uncp.endsWithSlash);
381 	}
382 
383 	{
384 		auto abspath = "/test/path/";
385 		auto abspathp = NativePath(abspath);
386 		assert(abspathp.toString() == abspath);
387 		version(Windows) {} else assert(abspathp.toNativeString() == abspath);
388 		assert(abspathp.absolute);
389 		assert(abspathp.endsWithSlash);
390 		assert(abspathp.length == 2);
391 		assert(abspathp[0] == "test");
392 		assert(abspathp[1] == "path");
393 	}
394 
395 	{
396 		auto relpath = "test/path/";
397 		auto relpathp = NativePath(relpath);
398 		assert(relpathp.toString() == relpath);
399 		version(Windows) assert(relpathp.toNativeString() == "test\\path\\");
400 		else assert(relpathp.toNativeString() == relpath);
401 		assert(!relpathp.absolute);
402 		assert(relpathp.endsWithSlash);
403 		assert(relpathp.length == 2);
404 		assert(relpathp[0] == "test");
405 		assert(relpathp[1] == "path");
406 	}
407 
408 	{
409 		auto winpath = "C:\\windows\\test";
410 		auto winpathp = NativePath(winpath);
411 		version(Windows) {
412 			assert(winpathp.toString() == "C:/windows/test", winpathp.toString());
413 			assert(winpathp.toNativeString() == winpath);
414 		} else {
415 			assert(winpathp.toString() == "/C:/windows/test", winpathp.toString());
416 			assert(winpathp.toNativeString() == "/C:/windows/test");
417 		}
418 		assert(winpathp.absolute);
419 		assert(!winpathp.endsWithSlash);
420 		assert(winpathp.length == 3);
421 		assert(winpathp[0] == "C:");
422 		assert(winpathp[1] == "windows");
423 		assert(winpathp[2] == "test");
424 	}
425 
426 	{
427 		auto dotpath = "/test/../test2/././x/y";
428 		auto dotpathp = NativePath(dotpath);
429 		assert(dotpathp.toString() == "/test/../test2/././x/y");
430 		dotpathp.normalize();
431 		assert(dotpathp.toString() == "/test2/x/y");
432 	}
433 
434 	{
435 		auto dotpath = "/test/..////test2//./x/y";
436 		auto dotpathp = NativePath(dotpath);
437 		assert(dotpathp.toString() == "/test/..////test2//./x/y");
438 		dotpathp.normalize();
439 		assert(dotpathp.toString() == "/test2/x/y");
440 	}
441 
442 	{
443 		auto parentpath = "/path/to/parent";
444 		auto parentpathp = NativePath(parentpath);
445 		auto subpath = "/path/to/parent/sub/";
446 		auto subpathp = NativePath(subpath);
447 		auto subpath_rel = "sub/";
448 		assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel);
449 		auto subfile = "/path/to/parent/child";
450 		auto subfilep = NativePath(subfile);
451 		auto subfile_rel = "child";
452 		assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel);
453 	}
454 
455 	{ // relative paths across Windows devices are not allowed
456 		version (Windows) {
457 			auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute);
458 			auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute);
459 			auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute);
460 			auto p4 = NativePath("C:\\somepath"); assert(p4.absolute);
461 			auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute);
462 			auto p6 = NativePath("D:\\somepath"); assert(p6.absolute);
463 			assert(p4.relativeTo(p5) == NativePath("../somepath"));
464 			assert(p4.relativeTo(p6) == NativePath("C:\\somepath"));
465 			assert(p4.relativeTo(p1) == NativePath("C:\\somepath"));
466 			assert(p1.relativeTo(p2) == NativePath("../share"));
467 			assert(p1.relativeTo(p3) == NativePath("\\\\server\\share"));
468 			assert(p1.relativeTo(p4) == NativePath("\\\\server\\share"));
469 		}
470 	}
471 }
472 
473 unittest {
474 	assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz");
475 	assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/");
476 	assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar");
477 	assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/");
478 	assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == "..");
479 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../");
480 	assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz");
481 	assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/");
482 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../");
483 	assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../");
484 	assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == "");
485 	assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == "");
486 }