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