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 		Appender!string ret;
101 		
102 		// for absolute unix paths start with /
103 		version(Posix) { if(absolute) ret.put('/'); }
104 		
105 		foreach( i, f; m_nodes ){
106 			version(Windows) { if( i > 0 ) ret.put('\\'); }
107 			version(Posix) { if( i > 0 ) ret.put('/'); }
108 			else { enforce("Unsupported OS"); }
109 			ret.put(f.toString());
110 		}
111 		
112 		if( m_nodes.length > 0 && m_endsWithSlash ){
113 			version(Windows) { ret.put('\\'); }
114 			version(Posix) { ret.put('/'); }
115 		}
116 		
117 		return ret.data;
118 	}
119 	
120 	/// Tests if `rhs` is an anchestor or the same as this path. 
121 	bool startsWith(const Path rhs) const {
122 		if( rhs.m_nodes.length > m_nodes.length ) return false;
123 		foreach( i; 0 .. rhs.m_nodes.length )
124 			if( m_nodes[i] != rhs.m_nodes[i] )
125 				return false;
126 		return true;
127 	}
128 	
129 	/// Computes the relative path from `parentPath` to this path.
130 	Path relativeTo(const Path parentPath) const {
131 		version(Windows){
132 			// a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case
133 			if( this.absolute && !this.empty && m_nodes[0].toString().endsWith(":") &&
134 				!parentPath.startsWith(this[0 .. 1]) )
135 			{
136 				return this;
137 			}
138 		}
139 		int nup = 0;
140 		while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){
141 			nup++;
142 		}
143 		Path ret = Path(null, false);
144 		ret.m_endsWithSlash = true;
145 		foreach( i; 0 .. nup ) ret ~= "..";
146 		ret ~= Path(m_nodes[parentPath.length-nup .. $], false);
147 		return ret;
148 	}
149 	
150 	/// The last entry of the path
151 	@property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; }
152 
153 	/// The parent path
154 	@property Path parentPath() const { return this[0 .. length-1]; }
155 
156 	/// The ist of path entries of which this path is composed
157 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
158 
159 	/// The number of path entries of which this path is composed
160 	@property size_t length() const { return m_nodes.length; }
161 
162 	/// True if the path contains no entries
163 	@property bool empty() const { return m_nodes.length == 0; }
164 
165 	/// Determines if the path ends with a slash (i.e. is a directory)
166 	@property bool endsWithSlash() const { return m_endsWithSlash; }
167 	/// ditto
168 	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
169 
170 	/// Determines if this path goes outside of its base path (i.e. begins with '..').
171 	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
172 		
173 	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
174 	Path opSlice(size_t start, size_t end) const {
175 		auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false);
176 		if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash;
177 		return ret;
178 	}
179 	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
180 	
181 	
182 	Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) {
183 		Path ret;
184 		ret.m_nodes = m_nodes;
185 		ret.m_absolute = m_absolute;
186 		ret.m_endsWithSlash = rhs.m_endsWithSlash;
187 		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
188 		
189 		assert(!rhs.absolute, "Trying to append absolute path.");
190 		size_t idx = m_nodes.length;
191 		foreach(folder; rhs.m_nodes){
192 			switch(folder.toString()){
193 				default: ret.m_nodes = ret.m_nodes ~ folder; break;
194 				case ".": break;
195 				case "..":
196 					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
197 					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
198 						ret.m_nodes = ret.m_nodes[0 .. $-1];
199 					else ret.m_nodes = ret.m_nodes ~ folder;
200 					break;
201 			}
202 		}
203 		return ret;
204 	}
205 	
206 	Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); }
207 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); }
208 	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); }
209 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); }
210 	void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; }
211 	
212 	/// Tests two paths for equality using '=='.
213 	bool opEquals(ref const Path rhs) const {
214 		if( m_absolute != rhs.m_absolute ) return false;
215 		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
216 		if( m_nodes.length != rhs.length ) return false;
217 		foreach( i; 0 .. m_nodes.length )
218 			if( m_nodes[i] != rhs.m_nodes[i] )
219 				return false;
220 		return true;
221 	}
222 	/// ditto
223 	bool opEquals(const Path other) const { return opEquals(other); }
224 
225 	int opCmp(ref const Path rhs) const {
226 		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
227 		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
228 			if( m_nodes[i] != rhs.m_nodes[i] )
229 				return m_nodes[i].opCmp(rhs.m_nodes[i]);
230 		if( m_nodes.length > rhs.m_nodes.length ) return 1;
231 		if( m_nodes.length < rhs.m_nodes.length ) return -1;
232 		return 0;
233 	}
234 }
235 
236 struct PathEntry {
237 	private {
238 		string m_name;
239 	}
240 	
241 	this(string str)
242 	{
243 		assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1));
244 		m_name = str;
245 	}
246 	
247 	string toString() const { return m_name; }
248 
249 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); }
250 	
251 	bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; }
252 	bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; }
253 	bool opEquals(string rhs) const { return m_name == rhs; }
254 	int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); }
255 	int opCmp(string rhs) const { return m_name.cmp(rhs); }
256 }
257 
258 private bool isValidFilename(string str)
259 {
260 	foreach( ch; str )
261 		if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false;
262 	return true;
263 }
264 
265 /// Joins two path strings. subpath must be relative.
266 string joinPath(string basepath, string subpath)
267 {
268 	Path p1 = Path(basepath);
269 	Path p2 = Path(subpath);
270 	return (p1 ~ p2).toString();
271 }
272 
273 /// Splits up a path string into its elements/folders
274 PathEntry[] splitPath(string path)
275 {
276 	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
277 	if( path.empty ) return null;
278 	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
279 
280 	// count the number of path nodes
281 	size_t nelements = 0;
282 	foreach( i, char ch; path )
283 		if( ch == '\\' || ch == '/' )
284 			nelements++;
285 	nelements++;
286 
287 	// reserve space for the elements
288 	auto elements = new PathEntry[nelements];
289 	size_t eidx = 0;
290 
291 	// detect UNC path
292 	if(path.startsWith("\\"))
293 	{
294 		elements[eidx++] = PathEntry(path[0 .. 1]);
295 		path = path[1 .. $];
296 	}
297 
298 	// read and return the elements
299 	size_t startidx = 0;
300 	foreach( i, char ch; path )
301 		if( ch == '\\' || ch == '/' ){
302 			enforce(i - startidx > 0, "Empty path entries not allowed.");
303 			elements[eidx++] = PathEntry(path[startidx .. i]);
304 			startidx = i+1;
305 		}
306 	elements[eidx++] = PathEntry(path[startidx .. $]);
307 	enforce(path.length - startidx > 0, "Empty path entries not allowed.");
308 	assert(eidx == nelements);
309 	return elements;
310 }
311 
312 unittest
313 {
314 	{
315 		auto unc = "\\\\server\\share\\path";
316 		auto uncp = Path(unc);
317 		version(Windows) assert(uncp.toNativeString() == unc);
318 		assert(uncp.absolute);
319 		assert(!uncp.endsWithSlash);
320 	}
321 
322 	{
323 		auto abspath = "/test/path/";
324 		auto abspathp = Path(abspath);
325 		assert(abspathp.toString() == abspath);
326 		version(Windows) {} else assert(abspathp.toNativeString() == abspath);
327 		assert(abspathp.absolute);
328 		assert(abspathp.endsWithSlash);
329 		assert(abspathp.length == 2);
330 		assert(abspathp[0] == "test");
331 		assert(abspathp[1] == "path");
332 	}
333 
334 	{
335 		auto relpath = "test/path/";
336 		auto relpathp = Path(relpath);
337 		assert(relpathp.toString() == relpath);
338 		version(Windows) assert(relpathp.toNativeString() == "test\\path\\");
339 		else assert(relpathp.toNativeString() == relpath);
340 		assert(!relpathp.absolute);
341 		assert(relpathp.endsWithSlash);
342 		assert(relpathp.length == 2);
343 		assert(relpathp[0] == "test");
344 		assert(relpathp[1] == "path");
345 	}
346 
347 	{
348 		auto winpath = "C:\\windows\\test";
349 		auto winpathp = Path(winpath);
350 		assert(winpathp.toString() == "/C:/windows/test");
351 		version(Windows) assert(winpathp.toNativeString() == winpath);
352 		else assert(winpathp.toNativeString() == "/C:/windows/test");
353 		assert(winpathp.absolute);
354 		assert(!winpathp.endsWithSlash);
355 		assert(winpathp.length == 3);
356 		assert(winpathp[0] == "C:");
357 		assert(winpathp[1] == "windows");
358 		assert(winpathp[2] == "test");
359 	}
360 
361 	{
362 		auto dotpath = "/test/../test2/././x/y";
363 		auto dotpathp = Path(dotpath);
364 		assert(dotpathp.toString() == "/test/../test2/././x/y");
365 		dotpathp.normalize();
366 		assert(dotpathp.toString() == "/test2/x/y");
367 	}
368 }