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