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