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