1 /**
2 	Contains routines for high level path handling.
3 
4 	Copyright: © 2012-2015 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 vibe.core.path;
9 
10 import std.algorithm : canFind, min;
11 import std.array;
12 import std.conv;
13 import std.exception;
14 import std.string;
15 
16 
17 /** Computes the relative path from `base_path` to this path.
18 
19 	Params:
20 		path = The destination path
21 		base_path = The path from which the relative path starts
22 
23 	See_also: `relativeToWeb`
24 */
25 Path relativeTo(Path path, Path base_path)
26 @safe{
27 	assert(path.absolute && base_path.absolute);
28 	version (Windows) {
29 		// a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case
30 		if (path.absolute && !path.empty &&
31 			(path[0].toString().endsWith(":") && !base_path.startsWith(path[0 .. 1]) ||
32 			path[0] == "\\" && !base_path.startsWith(path[0 .. min(2, $)])))
33 		{
34 			return path;
35 		}
36 	}
37 	int nup = 0;
38 	while (base_path.length > nup && !path.startsWith(base_path[0 .. base_path.length-nup])) {
39 		nup++;
40 	}
41 
42 	Path ret = Path(null, false);
43 	ret.m_endsWithSlash = true;
44 	foreach (i; 0 .. nup) ret ~= "..";
45 	ret ~= Path(path.nodes[base_path.length-nup .. $], false);
46 	ret.m_endsWithSlash = path.m_endsWithSlash;
47 	return ret;
48 }
49 
50 ///
51 unittest {
52 	assert(Path("/some/path").relativeTo(Path("/")) == Path("some/path"));
53 	assert(Path("/some/path/").relativeTo(Path("/some/other/path/")) == Path("../../path/"));
54 	assert(Path("/some/path/").relativeTo(Path("/some/other/path")) == Path("../../path/"));
55 }
56 
57 
58 /** Computes the relative path to this path from `base_path` using web path rules.
59 
60 	The difference to `relativeTo` is that a path not ending in a slash
61 	will not be considered as a path to a directory and the parent path
62 	will instead be used.
63 
64 	Params:
65 		path = The destination path
66 		base_path = The path from which the relative path starts
67 
68 	See_also: `relativeTo`
69 */
70 Path relativeToWeb(Path path, Path base_path)
71 @safe {
72 	if (!base_path.endsWithSlash) {
73 		if (base_path.length > 0) base_path = base_path[0 .. $-1];
74 		else base_path = Path("/");
75 	}
76 	return path.relativeTo(base_path);
77 }
78 
79 ///
80 unittest {
81 	assert(Path("/some/path").relativeToWeb(Path("/")) == Path("some/path"));
82 	assert(Path("/some/path/").relativeToWeb(Path("/some/other/path/")) == Path("../../path/"));
83 	assert(Path("/some/path/").relativeToWeb(Path("/some/other/path")) == Path("../path/"));
84 }
85 
86 
87 /// Forward compatibility alias for vibe-core
88 alias NativePath = Path;
89 /// ditto
90 alias PosixPath = Path;
91 /// ditto
92 alias WindowsPath = Path;
93 /// ditto
94 alias InetPath = Path;
95 
96 
97 /**
98 	Represents an absolute or relative file system path.
99 
100 	This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks
101 	are done to disallow invalid operations such as concatenating two absolute paths. It also
102 	validates path strings and allows for easy checking of malicious relative paths.
103 */
104 struct Path {
105 @safe:
106 	/// Forward compatibility alias for vibe-core
107 	alias Segment = PathEntry;
108 
109 	private {
110 		immutable(PathEntry)[] m_nodes;
111 		bool m_absolute = false;
112 		bool m_endsWithSlash = false;
113 	}
114 
115 	hash_t toHash()
116 	const nothrow @trusted {
117 		hash_t ret;
118 		auto strhash = &typeid(string).getHash;
119 		try foreach (n; nodes) ret ^= strhash(&n.m_name);
120 		catch (Throwable) assert(false);
121 		if (m_absolute) ret ^= 0xfe3c1738;
122 		if (m_endsWithSlash) ret ^= 0x6aa4352d;
123 		return ret;
124 	}
125 
126 	pure:
127 
128 	/// Constructs a Path object by parsing a path string.
129 	this(string pathstr)
130 	{
131 		m_nodes = splitPath(pathstr);
132 		m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && (m_nodes[0].toString().canFind(':') || m_nodes[0] == "\\"));
133 		m_endsWithSlash = pathstr.endsWith("/");
134 	}
135 
136 	/// Constructs a path object from a list of PathEntry objects.
137 	this(immutable(PathEntry)[] nodes, bool absolute)
138 	{
139 		m_nodes = nodes;
140 		m_absolute = absolute;
141 	}
142 
143 	/// Constructs a relative path with one path entry.
144 	this(PathEntry entry)
145 	{
146 		m_nodes = [entry];
147 		m_absolute = false;
148 	}
149 
150 	/// Determines if the path is absolute.
151 	@property bool absolute() const { return m_absolute; }
152 
153 	/// Forward compatibility property for vibe-code
154 	@property immutable(PathEntry)[] bySegment() { return nodes; }
155 
156 	/// Resolves all '.' and '..' path entries as far as possible.
157 	void normalize()
158 	{
159 		immutable(PathEntry)[] newnodes;
160 		foreach( n; m_nodes ){
161 			switch(n.toString()){
162 				default:
163 					newnodes ~= n;
164 					break;
165 				case "", ".": break;
166 				case "..":
167 					enforce(!m_absolute || newnodes.length > 0, "Path goes below root node.");
168 					if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1];
169 					else newnodes ~= n;
170 					break;
171 			}
172 		}
173 		m_nodes = newnodes;
174 	}
175 
176 	/// Converts the Path back to a string representation using slashes.
177 	string toString()
178 	const {
179 		if (m_nodes.empty)
180 			return absolute ? "/" : endsWithSlash ? "./" : "";
181 
182 		Appender!string ret;
183 
184 		// for absolute paths start with /
185 		if( absolute ) ret.put('/');
186 
187 		foreach( i, f; m_nodes ){
188 			if( i > 0 ) ret.put('/');
189 			ret.put(f.toString());
190 		}
191 
192 		if( m_nodes.length > 0 && m_endsWithSlash )
193 			ret.put('/');
194 
195 		return ret.data;
196 	}
197 
198 	/// Converts the Path object to a native path string (backslash as path separator on Windows).
199 	string toNativeString() nothrow
200 	const {
201 		Appender!string ret;
202 
203 		// for absolute unix paths start with /
204 		version(Posix) { if (m_absolute) ret.put('/'); }
205 
206 		foreach( i, f; m_nodes ){
207 			version(Windows) { if( i > 0 ) ret.put('\\'); }
208 			else version(Posix) { if( i > 0 ) ret.put('/'); }
209 			else static assert(false, "Unsupported OS");
210 			ret.put(f.toString());
211 		}
212 
213 		if( m_nodes.length > 0 && m_endsWithSlash ){
214 			version(Windows) { ret.put('\\'); }
215 			version(Posix) { ret.put('/'); }
216 		}
217 
218 		return ret.data;
219 	}
220 
221 	/// Tests if `rhs` is an anchestor or the same as this path.
222 	bool startsWith(const Path rhs) const {
223 		if( rhs.m_nodes.length > m_nodes.length ) return false;
224 		foreach( i; 0 .. rhs.m_nodes.length )
225 			if( m_nodes[i] != rhs.m_nodes[i] )
226 				return false;
227 		return true;
228 	}
229 
230 	/// The last entry of the path
231 	@property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; }
232 
233 	/// The parent path
234 	@property Path parentPath() const { return this[0 .. length-1]; }
235 
236 	/// The ist of path entries of which this path is composed
237 	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
238 
239 	/// The number of path entries of which this path is composed
240 	@property size_t length() const { return m_nodes.length; }
241 
242 	/// True if the path contains no entries
243 	@property bool empty() const { return m_nodes.length == 0; }
244 
245 	/// Determines if the path ends with a slash (i.e. is a directory)
246 	@property bool endsWithSlash() const { return m_endsWithSlash; }
247 	/// ditto
248 	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
249 
250 	/// Determines if this path goes outside of its base path (i.e. begins with '..').
251 	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
252 
253 	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
254 	Path opSlice(size_t start, size_t end) const {
255 		auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false);
256 		ret.m_endsWithSlash = end == m_nodes.length ? m_endsWithSlash : true;
257 		return ret;
258 	}
259 	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
260 
261 
262 	Path opBinary(string OP)(const Path rhs) const if( OP == "~" )
263 	{
264 		assert(!rhs.absolute, "Trying to append absolute path.");
265 		if (!rhs.length) return this;
266 
267 		Path ret;
268 		ret.m_nodes = m_nodes;
269 		ret.m_absolute = m_absolute;
270 		ret.m_endsWithSlash = rhs.m_endsWithSlash;
271 		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
272 
273 		foreach (folder; rhs.m_nodes) {
274 			switch (folder.toString()) {
275 				default: ret.m_nodes = ret.m_nodes ~ folder; break;
276 				case "", ".": break;
277 				case "..":
278 					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
279 					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
280 						ret.m_nodes = ret.m_nodes[0 .. $-1];
281 					else ret.m_nodes = ret.m_nodes ~ folder;
282 					break;
283 			}
284 		}
285 		return ret;
286 	}
287 
288 	Path opBinary(string OP)(string rhs) const if( OP == "~" ) { return opBinary!"~"(Path(rhs)); }
289 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return opBinary!"~"(Path(rhs)); }
290 	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { opOpAssign!"~"(Path(rhs)); }
291 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { opOpAssign!"~"(Path(rhs)); }
292 	void opOpAssign(string OP)(immutable(PathEntry)[] rhs) if( OP == "~" ) { opOpAssign!"~"(Path(rhs, false)); }
293 	void opOpAssign(string OP)(Path rhs) if( OP == "~" )
294 	{
295 		assert(!rhs.absolute, "Trying to append absolute path.");
296 		if (!rhs.length) return;
297 		auto p = this ~ rhs;
298 		m_nodes = p.m_nodes;
299 		m_endsWithSlash = rhs.m_endsWithSlash;
300 	}
301 
302 	/// Tests two paths for equality using '=='.
303 	bool opEquals(ref const Path rhs) const {
304 		if( m_absolute != rhs.m_absolute ) return false;
305 		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
306 		if( m_nodes.length != rhs.length ) return false;
307 		foreach( i; 0 .. m_nodes.length )
308 			if( m_nodes[i] != rhs.m_nodes[i] )
309 				return false;
310 		return true;
311 	}
312 	/// ditto
313 	bool opEquals(const Path other) const { return opEquals(other); }
314 
315 	int opCmp(ref const Path rhs) const {
316 		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
317 		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
318 			if( m_nodes[i] != rhs.m_nodes[i] )
319 				return m_nodes[i].opCmp(rhs.m_nodes[i]);
320 		if( m_nodes.length > rhs.m_nodes.length ) return 1;
321 		if( m_nodes.length < rhs.m_nodes.length ) return -1;
322 		return 0;
323 	}
324 }
325 
326 
327 unittest
328 {
329 	{
330 		auto unc = "\\\\server\\share\\path";
331 		auto uncp = Path(unc);
332 		uncp.normalize();
333 		version(Windows) assert(uncp.toNativeString() == unc);
334 		assert(uncp.absolute);
335 		assert(!uncp.endsWithSlash);
336 	}
337 
338 	{
339 		auto abspath = "/test/path/";
340 		auto abspathp = Path(abspath);
341 		assert(abspathp.toString() == abspath);
342 		version(Windows) {} else assert(abspathp.toNativeString() == abspath);
343 		assert(abspathp.absolute);
344 		assert(abspathp.endsWithSlash);
345 		assert(abspathp.length == 2);
346 		assert(abspathp[0] == "test");
347 		assert(abspathp[1] == "path");
348 	}
349 
350 	{
351 		auto relpath = "test/path/";
352 		auto relpathp = Path(relpath);
353 		assert(relpathp.toString() == relpath);
354 		version(Windows) assert(relpathp.toNativeString() == "test\\path\\");
355 		else assert(relpathp.toNativeString() == relpath);
356 		assert(!relpathp.absolute);
357 		assert(relpathp.endsWithSlash);
358 		assert(relpathp.length == 2);
359 		assert(relpathp[0] == "test");
360 		assert(relpathp[1] == "path");
361 	}
362 
363 	{
364 		auto winpath = "C:\\windows\\test";
365 		auto winpathp = Path(winpath);
366 		assert(winpathp.toString() == "/C:/windows/test");
367 		version(Windows) assert(winpathp.toNativeString() == winpath);
368 		else assert(winpathp.toNativeString() == "/C:/windows/test");
369 		assert(winpathp.absolute);
370 		assert(!winpathp.endsWithSlash);
371 		assert(winpathp.length == 3);
372 		assert(winpathp[0] == "C:");
373 		assert(winpathp[1] == "windows");
374 		assert(winpathp[2] == "test");
375 	}
376 
377 	{
378 		auto dotpath = "/test/../test2/././x/y";
379 		auto dotpathp = Path(dotpath);
380 		assert(dotpathp.toString() == "/test/../test2/././x/y");
381 		dotpathp.normalize();
382 		assert(dotpathp.toString() == "/test2/x/y");
383 	}
384 
385 	{
386 		auto dotpath = "/test/..////test2//./x/y";
387 		auto dotpathp = Path(dotpath);
388 		assert(dotpathp.toString() == "/test/..////test2//./x/y");
389 		dotpathp.normalize();
390 		assert(dotpathp.toString() == "/test2/x/y");
391 	}
392 
393 	{
394 		auto parentpath = "/path/to/parent";
395 		auto parentpathp = Path(parentpath);
396 		auto subpath = "/path/to/parent/sub/";
397 		auto subpathp = Path(subpath);
398 		auto subpath_rel = "sub/";
399 		assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel);
400 		auto subfile = "/path/to/parent/child";
401 		auto subfilep = Path(subfile);
402 		auto subfile_rel = "child";
403 		assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel);
404 	}
405 
406 	{ // relative paths across Windows devices are not allowed
407 		version (Windows) {
408 			auto p1 = Path("\\\\server\\share"); assert(p1.absolute);
409 			auto p2 = Path("\\\\server\\othershare"); assert(p2.absolute);
410 			auto p3 = Path("\\\\otherserver\\share"); assert(p3.absolute);
411 			auto p4 = Path("C:\\somepath"); assert(p4.absolute);
412 			auto p5 = Path("C:\\someotherpath"); assert(p5.absolute);
413 			auto p6 = Path("D:\\somepath"); assert(p6.absolute);
414 			assert(p4.relativeTo(p5) == Path("../somepath"));
415 			assert(p4.relativeTo(p6) == Path("C:\\somepath"));
416 			assert(p4.relativeTo(p1) == Path("C:\\somepath"));
417 			assert(p1.relativeTo(p2) == Path("../share"));
418 			assert(p1.relativeTo(p3) == Path("\\\\server\\share"));
419 			assert(p1.relativeTo(p4) == Path("\\\\server\\share"));
420 		}
421 	}
422 
423 	{ // relative path, trailing slash
424 		auto p1 = Path("/some/path");
425 		auto p2 = Path("/some/path/");
426 		assert(p1.relativeTo(p1).toString() == "");
427 		assert(p1.relativeTo(p2).toString() == "");
428 		assert(p2.relativeTo(p2).toString() == "./");
429 		assert(p2.relativeTo(p1).toString() == "./");
430 	}
431 }
432 
433 
434 struct PathEntry {
435 @safe: pure:
436 
437 	private {
438 		string m_name;
439 	}
440 
441 	static PathEntry validateFilename(string fname)
442 	{
443 		enforce(fname.indexOfAny("/\\") < 0, "File name contains forward or backward slashes: "~fname);
444 		return PathEntry(fname);
445 	}
446 
447 	this(string str)
448 	{
449 		assert(!str.canFind('/') && (!str.canFind('\\') || str.length == 1), "Invalid path entry: " ~ str);
450 		m_name = str;
451 	}
452 
453 	string toString() const nothrow { return m_name; }
454 
455 	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path([this, rhs], false); }
456 
457 	@property string name() const nothrow { return m_name; }
458 
459 	bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; }
460 	bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; }
461 	bool opEquals(string rhs) const { return m_name == rhs; }
462 	int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); }
463 	int opCmp(string rhs) const { return m_name.cmp(rhs); }
464 }
465 
466 private bool isValidFilename(string str)
467 pure @safe {
468 	foreach( ch; str )
469 		if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false;
470 	return true;
471 }
472 
473 /// Joins two path strings. subpath must be relative.
474 string joinPath(string basepath, string subpath)
475 pure @safe {
476 	Path p1 = Path(basepath);
477 	Path p2 = Path(subpath);
478 	return (p1 ~ p2).toString();
479 }
480 
481 /// Splits up a path string into its elements/folders
482 PathEntry[] splitPath(string path)
483 pure @safe {
484 	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
485 	if( path.empty ) return null;
486 	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
487 
488 	// count the number of path nodes
489 	size_t nelements = 0;
490 	foreach( i, char ch; path )
491 		if( ch == '\\' || ch == '/' )
492 			nelements++;
493 	nelements++;
494 
495 	// reserve space for the elements
496 	PathEntry[] storage;
497 	/*if (alloc) {
498 		auto mem = alloc.alloc(nelements * PathEntry.sizeof);
499 		mem[] = 0;
500 		storage = cast(PathEntry[])mem;
501 	} else*/ storage = new PathEntry[nelements];
502 
503 	size_t startidx = 0;
504 	size_t eidx = 0;
505 
506 	// detect UNC path
507 	if(path.startsWith("\\"))
508 	{
509 		storage[eidx++] = PathEntry(path[0 .. 1]);
510 		path = path[1 .. $];
511 	}
512 
513 	// read and return the elements
514 	foreach( i, char ch; path )
515 		if( ch == '\\' || ch == '/' ){
516 			storage[eidx++] = PathEntry(path[startidx .. i]);
517 			startidx = i+1;
518 		}
519 	storage[eidx++] = PathEntry(path[startidx .. $]);
520 	assert(eidx == nelements);
521 	return storage;
522 }