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