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: 12 13 import std.algorithm; 14 import std.array; 15 import std.conv; 16 import std.exception; 17 import std.string; 18 19 20 deprecated("Use NativePath instead.") 21 alias Path = NativePath; 22 23 /** 24 Represents an absolute or relative file system path. 25 26 This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks 27 are done to disallow invalid operations such as concatenating two absolute paths. It also 28 validates path strings and allows for easy checking of malicious relative paths. 29 */ 30 struct NativePath { 31 private { 32 immutable(PathEntry)[] m_nodes; 33 bool m_absolute = false; 34 bool m_endsWithSlash = false; 35 } 36 37 alias Segment = PathEntry; 38 39 alias bySegment = nodes; 40 41 /// Constructs a NativePath object by parsing a path string. 42 this(string pathstr) 43 { 44 m_nodes = splitPath(pathstr); 45 m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && (m_nodes[0].toString().countUntil(':')>0 || m_nodes[0] == "\\")); 46 m_endsWithSlash = pathstr.endsWith("/"); 47 } 48 49 /// Constructs a path object from a list of PathEntry objects. 50 this(immutable(PathEntry)[] nodes, bool absolute) 51 { 52 m_nodes = nodes; 53 m_absolute = absolute; 54 } 55 56 /// Constructs a relative path with one path entry. 57 this(PathEntry entry){ 58 m_nodes = [entry]; 59 m_absolute = false; 60 } 61 62 /// Determines if the path is absolute. 63 @property bool absolute() const scope @safe pure nothrow @nogc { return m_absolute; } 64 65 /// Resolves all '.' and '..' path entries as far as possible. 66 void normalize() 67 { 68 immutable(PathEntry)[] newnodes; 69 foreach( n; m_nodes ){ 70 switch(n.toString()){ 71 default: 72 newnodes ~= n; 73 break; 74 case "", ".": break; 75 case "..": 76 enforce(!m_absolute || newnodes.length > 0, "Path goes below root node."); 77 if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; 78 else newnodes ~= n; 79 break; 80 } 81 } 82 m_nodes = newnodes; 83 } 84 85 /// Converts the Path back to a string representation using slashes. 86 string toString() 87 const @safe { 88 if( m_nodes.empty ) return absolute ? "/" : ""; 89 90 Appender!string ret; 91 92 // for absolute paths start with / 93 version(Windows) 94 { 95 // Make sure windows path isn't "DRIVE:" 96 if( absolute && !m_nodes[0].toString().endsWith(':') ) 97 ret.put('/'); 98 } 99 else 100 { 101 if( absolute ) 102 { 103 ret.put('/'); 104 } 105 } 106 107 foreach( i, f; m_nodes ){ 108 if( i > 0 ) ret.put('/'); 109 ret.put(f.toString()); 110 } 111 112 if( m_nodes.length > 0 && m_endsWithSlash ) 113 ret.put('/'); 114 115 return ret.data; 116 } 117 118 /// Converts the NativePath object to a native path string (backslash as path separator on Windows). 119 string toNativeString() 120 const { 121 if (m_nodes.empty) { 122 version(Windows) { 123 assert(!absolute, "Empty absolute path detected."); 124 return m_endsWithSlash ? ".\\" : "."; 125 } else return absolute ? "/" : m_endsWithSlash ? "./" : "."; 126 } 127 128 Appender!string ret; 129 130 // for absolute unix paths start with / 131 version(Posix) { if(absolute) ret.put('/'); } 132 133 foreach( i, f; m_nodes ){ 134 version(Windows) { if( i > 0 ) ret.put('\\'); } 135 else version(Posix) { if( i > 0 ) ret.put('/'); } 136 else { static assert(0, "Unsupported OS"); } 137 ret.put(f.toString()); 138 } 139 140 if( m_nodes.length > 0 && m_endsWithSlash ){ 141 version(Windows) { ret.put('\\'); } 142 version(Posix) { ret.put('/'); } 143 } 144 145 return ret.data; 146 } 147 148 /// Tests if `rhs` is an anchestor or the same as this path. 149 bool startsWith(const NativePath rhs) const { 150 if( rhs.m_nodes.length > m_nodes.length ) return false; 151 foreach( i; 0 .. rhs.m_nodes.length ) 152 if( m_nodes[i] != rhs.m_nodes[i] ) 153 return false; 154 return true; 155 } 156 157 /// Computes the relative path from `parentPath` to this path. 158 NativePath relativeTo(const NativePath parentPath) const { 159 assert(this.absolute && parentPath.absolute, "Determining relative path between non-absolute paths."); 160 version(Windows){ 161 // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case 162 if( this.absolute && !this.empty && 163 (m_nodes[0].toString().endsWith(":") && !parentPath.startsWith(this[0 .. 1]) || 164 m_nodes[0] == "\\" && !parentPath.startsWith(this[0 .. min(2, $)]))) 165 { 166 return this; 167 } 168 } 169 int nup = 0; 170 while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ 171 nup++; 172 } 173 assert(m_nodes.length >= parentPath.length - nup); 174 NativePath ret = NativePath(null, false); 175 assert(m_nodes.length >= parentPath.length - nup); 176 ret.m_endsWithSlash = true; 177 foreach( i; 0 .. nup ) ret ~= ".."; 178 ret ~= NativePath(m_nodes[parentPath.length-nup .. $], false); 179 ret.m_endsWithSlash = this.m_endsWithSlash; 180 return ret; 181 } 182 183 /// The last entry of the path 184 @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0, "Getting head of empty path."); return m_nodes[$-1]; } 185 186 /// The parent path 187 @property NativePath parentPath() const { return this[0 .. length-1]; } 188 189 /// The ist of path entries of which this path is composed 190 @property immutable(PathEntry)[] nodes() const { return m_nodes; } 191 192 /// The number of path entries of which this path is composed 193 @property size_t length() const scope @safe pure nothrow @nogc { return m_nodes.length; } 194 195 /// True if the path contains no entries 196 @property bool empty() const scope @safe pure nothrow @nogc { return m_nodes.length == 0; } 197 198 /// Determines if the path ends with a slash (i.e. is a directory) 199 @property bool endsWithSlash() const { return m_endsWithSlash; } 200 /// ditto 201 @property void endsWithSlash(bool v) { m_endsWithSlash = v; } 202 203 /// Determines if this path goes outside of its base path (i.e. begins with '..'). 204 @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } 205 206 ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } 207 NativePath opSlice(size_t start, size_t end) const { 208 auto ret = NativePath(m_nodes[start .. end], start == 0 ? absolute : false); 209 if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; 210 return ret; 211 } 212 size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } 213 214 215 NativePath opBinary(string OP)(const NativePath rhs) const if( OP == "~" ) { 216 NativePath ret; 217 ret.m_nodes = m_nodes; 218 ret.m_absolute = m_absolute; 219 ret.m_endsWithSlash = rhs.m_endsWithSlash; 220 ret.normalize(); // needed to avoid "."~".." become "" instead of ".." 221 222 assert(!rhs.absolute, "Trying to append absolute path."); 223 foreach(folder; rhs.m_nodes){ 224 switch(folder.toString()){ 225 default: ret.m_nodes = ret.m_nodes ~ folder; break; 226 case "", ".": break; 227 case "..": 228 enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); 229 if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) 230 ret.m_nodes = ret.m_nodes[0 .. $-1]; 231 else ret.m_nodes = ret.m_nodes ~ folder; 232 break; 233 } 234 } 235 return ret; 236 } 237 238 NativePath opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } 239 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); return opBinary!"~"(NativePath(rhs)); } 240 void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } 241 void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0, "Cannot append empty path string."); opOpAssign!"~"(NativePath(rhs)); } 242 void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } 243 244 /// Tests two paths for equality using '=='. 245 bool opEquals(scope ref const NativePath rhs) const scope @safe { 246 if( m_absolute != rhs.m_absolute ) return false; 247 if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; 248 if( m_nodes.length != rhs.length ) return false; 249 foreach( i; 0 .. m_nodes.length ) 250 if( m_nodes[i] != rhs.m_nodes[i] ) 251 return false; 252 return true; 253 } 254 /// ditto 255 bool opEquals(scope const NativePath other) const scope @safe { return opEquals(other); } 256 257 int opCmp(ref const NativePath rhs) const { 258 if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; 259 foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) 260 if( m_nodes[i] != rhs.m_nodes[i] ) 261 return m_nodes[i].opCmp(rhs.m_nodes[i]); 262 if( m_nodes.length > rhs.m_nodes.length ) return 1; 263 if( m_nodes.length < rhs.m_nodes.length ) return -1; 264 return 0; 265 } 266 267 size_t toHash() 268 const nothrow @trusted { 269 size_t ret; 270 auto strhash = &typeid(string).getHash; 271 try foreach (n; nodes) ret ^= strhash(&n.m_name); 272 catch (Exception) assert(false); 273 if (m_absolute) ret ^= 0xfe3c1738; 274 if (m_endsWithSlash) ret ^= 0x6aa4352d; 275 return ret; 276 } 277 } 278 279 struct PathEntry { 280 private { 281 string m_name; 282 } 283 284 this(string str) 285 pure { 286 assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1)); 287 m_name = str; 288 } 289 290 string toString() const return scope @safe pure nothrow @nogc { return m_name; } 291 292 @property string name() const return scope @safe pure nothrow @nogc { return m_name; } 293 294 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); } 295 296 bool opEquals(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } 297 bool opEquals(scope PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } 298 bool opEquals(string rhs) const scope @safe pure nothrow @nogc { return m_name == rhs; } 299 int opCmp(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs.m_name); } 300 int opCmp(string rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs); } 301 } 302 303 /// Joins two path strings. subpath must be relative. 304 string joinPath(string basepath, string subpath) 305 { 306 NativePath p1 = NativePath(basepath); 307 NativePath p2 = NativePath(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 NativePath p; 351 assert(p.toNativeString() == "."); 352 p.endsWithSlash = true; 353 version(Windows) assert(p.toNativeString() == ".\\"); 354 else assert(p.toNativeString() == "./"); 355 356 p = NativePath("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 = NativePath(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 = NativePath(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 = NativePath(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 = NativePath(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 = NativePath(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 = 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 parentpath = "/path/to/parent"; 435 auto parentpathp = NativePath(parentpath); 436 auto subpath = "/path/to/parent/sub/"; 437 auto subpathp = NativePath(subpath); 438 auto subpath_rel = "sub/"; 439 assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); 440 auto subfile = "/path/to/parent/child"; 441 auto subfilep = NativePath(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 = NativePath("\\\\server\\share"); assert(p1.absolute); 449 auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute); 450 auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute); 451 auto p4 = NativePath("C:\\somepath"); assert(p4.absolute); 452 auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute); 453 auto p6 = NativePath("D:\\somepath"); assert(p6.absolute); 454 assert(p4.relativeTo(p5) == NativePath("../somepath")); 455 assert(p4.relativeTo(p6) == NativePath("C:\\somepath")); 456 assert(p4.relativeTo(p1) == NativePath("C:\\somepath")); 457 assert(p1.relativeTo(p2) == NativePath("../share")); 458 assert(p1.relativeTo(p3) == NativePath("\\\\server\\share")); 459 assert(p1.relativeTo(p4) == NativePath("\\\\server\\share")); 460 } 461 } 462 } 463 464 unittest { 465 assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz"); 466 assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/"); 467 assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar"); 468 assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/"); 469 assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == ".."); 470 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../"); 471 assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz"); 472 assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/"); 473 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../"); 474 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../"); 475 assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == ""); 476 assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == ""); 477 }