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