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