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 foreach(folder; rhs.m_nodes){ 225 switch(folder.toString()){ 226 default: ret.m_nodes = ret.m_nodes ~ folder; break; 227 case "", ".": break; 228 case "..": 229 enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); 230 if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) 231 ret.m_nodes = ret.m_nodes[0 .. $-1]; 232 else ret.m_nodes = ret.m_nodes ~ folder; 233 break; 234 } 235 } 236 return ret; 237 } 238 239 NativePath opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } 240 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } 241 void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } 242 void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } 243 void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } 244 245 /// Tests two paths for equality using '=='. 246 bool opEquals(ref const NativePath rhs) const { 247 if( m_absolute != rhs.m_absolute ) return false; 248 if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; 249 if( m_nodes.length != rhs.length ) return false; 250 foreach( i; 0 .. m_nodes.length ) 251 if( m_nodes[i] != rhs.m_nodes[i] ) 252 return false; 253 return true; 254 } 255 /// ditto 256 bool opEquals(const NativePath other) const { return opEquals(other); } 257 258 int opCmp(ref const NativePath rhs) const { 259 if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; 260 foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) 261 if( m_nodes[i] != rhs.m_nodes[i] ) 262 return m_nodes[i].opCmp(rhs.m_nodes[i]); 263 if( m_nodes.length > rhs.m_nodes.length ) return 1; 264 if( m_nodes.length < rhs.m_nodes.length ) return -1; 265 return 0; 266 } 267 268 size_t toHash() 269 const nothrow @trusted { 270 size_t ret; 271 auto strhash = &typeid(string).getHash; 272 try foreach (n; nodes) ret ^= strhash(&n.m_name); 273 catch (Exception) assert(false); 274 if (m_absolute) ret ^= 0xfe3c1738; 275 if (m_endsWithSlash) ret ^= 0x6aa4352d; 276 return ret; 277 } 278 } 279 280 struct PathEntry { 281 private { 282 string m_name; 283 } 284 285 this(string str) 286 pure { 287 assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1)); 288 m_name = str; 289 } 290 291 string toString() const pure { return m_name; } 292 293 @property string name() const { return m_name; } 294 295 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); } 296 297 bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } 298 bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } 299 bool opEquals(string rhs) const { return m_name == rhs; } 300 int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } 301 int opCmp(string rhs) const { return m_name.cmp(rhs); } 302 } 303 304 private bool isValidFilename(string str) 305 { 306 foreach( ch; str ) 307 if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; 308 return true; 309 } 310 311 /// Joins two path strings. subpath must be relative. 312 string joinPath(string basepath, string subpath) 313 { 314 NativePath p1 = NativePath(basepath); 315 NativePath p2 = NativePath(subpath); 316 return (p1 ~ p2).toString(); 317 } 318 319 /// Splits up a path string into its elements/folders 320 PathEntry[] splitPath(string path) 321 pure { 322 if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; 323 if( path.empty ) return null; 324 if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; 325 326 // count the number of path nodes 327 size_t nelements = 0; 328 foreach( i, char ch; path ) 329 if( ch == '\\' || ch == '/' ) 330 nelements++; 331 nelements++; 332 333 // reserve space for the elements 334 auto elements = new PathEntry[nelements]; 335 size_t eidx = 0; 336 337 // detect UNC path 338 if(path.startsWith("\\")) 339 { 340 elements[eidx++] = PathEntry(path[0 .. 1]); 341 path = path[1 .. $]; 342 } 343 344 // read and return the elements 345 size_t startidx = 0; 346 foreach( i, char ch; path ) 347 if( ch == '\\' || ch == '/' ){ 348 elements[eidx++] = PathEntry(path[startidx .. i]); 349 startidx = i+1; 350 } 351 elements[eidx++] = PathEntry(path[startidx .. $]); 352 assert(eidx == nelements); 353 return elements; 354 } 355 356 unittest 357 { 358 NativePath p; 359 assert(p.toNativeString() == "."); 360 p.endsWithSlash = true; 361 version(Windows) assert(p.toNativeString() == ".\\"); 362 else assert(p.toNativeString() == "./"); 363 364 p = NativePath("test/"); 365 version(Windows) assert(p.toNativeString() == "test\\"); 366 else assert(p.toNativeString() == "test/"); 367 p.endsWithSlash = false; 368 assert(p.toNativeString() == "test"); 369 } 370 371 unittest 372 { 373 { 374 auto unc = "\\\\server\\share\\path"; 375 auto uncp = NativePath(unc); 376 uncp.normalize(); 377 version(Windows) assert(uncp.toNativeString() == unc); 378 assert(uncp.absolute); 379 assert(!uncp.endsWithSlash); 380 } 381 382 { 383 auto abspath = "/test/path/"; 384 auto abspathp = NativePath(abspath); 385 assert(abspathp.toString() == abspath); 386 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 387 assert(abspathp.absolute); 388 assert(abspathp.endsWithSlash); 389 assert(abspathp.length == 2); 390 assert(abspathp[0] == "test"); 391 assert(abspathp[1] == "path"); 392 } 393 394 { 395 auto relpath = "test/path/"; 396 auto relpathp = NativePath(relpath); 397 assert(relpathp.toString() == relpath); 398 version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); 399 else assert(relpathp.toNativeString() == relpath); 400 assert(!relpathp.absolute); 401 assert(relpathp.endsWithSlash); 402 assert(relpathp.length == 2); 403 assert(relpathp[0] == "test"); 404 assert(relpathp[1] == "path"); 405 } 406 407 { 408 auto winpath = "C:\\windows\\test"; 409 auto winpathp = NativePath(winpath); 410 version(Windows) { 411 assert(winpathp.toString() == "C:/windows/test", winpathp.toString()); 412 assert(winpathp.toNativeString() == winpath); 413 } else { 414 assert(winpathp.toString() == "/C:/windows/test", winpathp.toString()); 415 assert(winpathp.toNativeString() == "/C:/windows/test"); 416 } 417 assert(winpathp.absolute); 418 assert(!winpathp.endsWithSlash); 419 assert(winpathp.length == 3); 420 assert(winpathp[0] == "C:"); 421 assert(winpathp[1] == "windows"); 422 assert(winpathp[2] == "test"); 423 } 424 425 { 426 auto dotpath = "/test/../test2/././x/y"; 427 auto dotpathp = NativePath(dotpath); 428 assert(dotpathp.toString() == "/test/../test2/././x/y"); 429 dotpathp.normalize(); 430 assert(dotpathp.toString() == "/test2/x/y"); 431 } 432 433 { 434 auto dotpath = "/test/..////test2//./x/y"; 435 auto dotpathp = NativePath(dotpath); 436 assert(dotpathp.toString() == "/test/..////test2//./x/y"); 437 dotpathp.normalize(); 438 assert(dotpathp.toString() == "/test2/x/y"); 439 } 440 441 { 442 auto parentpath = "/path/to/parent"; 443 auto parentpathp = NativePath(parentpath); 444 auto subpath = "/path/to/parent/sub/"; 445 auto subpathp = NativePath(subpath); 446 auto subpath_rel = "sub/"; 447 assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); 448 auto subfile = "/path/to/parent/child"; 449 auto subfilep = NativePath(subfile); 450 auto subfile_rel = "child"; 451 assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); 452 } 453 454 { // relative paths across Windows devices are not allowed 455 version (Windows) { 456 auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute); 457 auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute); 458 auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute); 459 auto p4 = NativePath("C:\\somepath"); assert(p4.absolute); 460 auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute); 461 auto p6 = NativePath("D:\\somepath"); assert(p6.absolute); 462 assert(p4.relativeTo(p5) == NativePath("../somepath")); 463 assert(p4.relativeTo(p6) == NativePath("C:\\somepath")); 464 assert(p4.relativeTo(p1) == NativePath("C:\\somepath")); 465 assert(p1.relativeTo(p2) == NativePath("../share")); 466 assert(p1.relativeTo(p3) == NativePath("\\\\server\\share")); 467 assert(p1.relativeTo(p4) == NativePath("\\\\server\\share")); 468 } 469 } 470 } 471 472 unittest { 473 assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz"); 474 assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/"); 475 assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar"); 476 assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/"); 477 assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == ".."); 478 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../"); 479 assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz"); 480 assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/"); 481 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../"); 482 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../"); 483 assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == ""); 484 assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == ""); 485 }