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 }