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