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 = false) 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 ancestor 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 /// Forward compatibility with vibe-d 189 @property bool hasParentPath() const { return length > 1; } 190 191 /// The list of path entries of which this path is composed 192 @property immutable(PathEntry)[] nodes() const { return m_nodes; } 193 194 /// The number of path entries of which this path is composed 195 @property size_t length() const scope @safe pure nothrow @nogc { return m_nodes.length; } 196 197 /// True if the path contains no entries 198 @property bool empty() const scope @safe pure nothrow @nogc { return m_nodes.length == 0; } 199 200 /// Determines if the path ends with a slash (i.e. is a directory) 201 @property bool endsWithSlash() const { return m_endsWithSlash; } 202 /// ditto 203 @property void endsWithSlash(bool v) { m_endsWithSlash = v; } 204 205 /// Determines if this path goes outside of its base path (i.e. begins with '..'). 206 @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } 207 208 ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } 209 NativePath opSlice(size_t start, size_t end) const { 210 auto ret = NativePath(m_nodes[start .. end], start == 0 ? absolute : false); 211 if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; 212 return ret; 213 } 214 size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } 215 216 217 NativePath opBinary(string OP)(const NativePath rhs) const if( OP == "~" ) { 218 NativePath ret; 219 ret.m_nodes = m_nodes; 220 ret.m_absolute = m_absolute; 221 ret.m_endsWithSlash = rhs.m_endsWithSlash; 222 ret.normalize(); // needed to avoid "."~".." become "" instead of ".." 223 224 assert(!rhs.absolute, "Trying to append absolute path."); 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(scope ref const NativePath rhs) const scope @safe { 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(scope const NativePath other) const scope @safe { 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 size_t toHash() 270 const nothrow @trusted { 271 size_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 return scope @safe pure nothrow @nogc { return m_name; } 293 294 @property string name() const return scope @safe pure nothrow @nogc { return m_name; } 295 296 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); } 297 298 bool opEquals(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } 299 bool opEquals(scope PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name == rhs.m_name; } 300 bool opEquals(string rhs) const scope @safe pure nothrow @nogc { return m_name == rhs; } 301 int opCmp(scope ref const PathEntry rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs.m_name); } 302 int opCmp(string rhs) const scope @safe pure nothrow @nogc { return m_name.cmp(rhs); } 303 } 304 305 /// Joins two path strings. sub-path must be relative. 306 string joinPath(string basepath, string subpath) 307 { 308 NativePath p1 = NativePath(basepath); 309 NativePath p2 = NativePath(subpath); 310 return (p1 ~ p2).toString(); 311 } 312 313 /// Splits up a path string into its elements/folders 314 PathEntry[] splitPath(string path) 315 pure { 316 if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; 317 if( path.empty ) return null; 318 if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; 319 320 // count the number of path nodes 321 size_t nelements = 0; 322 foreach( i, char ch; path ) 323 if( ch == '\\' || ch == '/' ) 324 nelements++; 325 nelements++; 326 327 // reserve space for the elements 328 auto elements = new PathEntry[nelements]; 329 size_t eidx = 0; 330 331 // detect UNC path 332 if(path.startsWith("\\")) 333 { 334 elements[eidx++] = PathEntry(path[0 .. 1]); 335 path = path[1 .. $]; 336 } 337 338 // read and return the elements 339 size_t startidx = 0; 340 foreach( i, char ch; path ) 341 if( ch == '\\' || ch == '/' ){ 342 elements[eidx++] = PathEntry(path[startidx .. i]); 343 startidx = i+1; 344 } 345 elements[eidx++] = PathEntry(path[startidx .. $]); 346 assert(eidx == nelements); 347 return elements; 348 } 349 350 unittest 351 { 352 NativePath p; 353 assert(p.toNativeString() == "."); 354 p.endsWithSlash = true; 355 version(Windows) assert(p.toNativeString() == ".\\"); 356 else assert(p.toNativeString() == "./"); 357 358 p = NativePath("test/"); 359 version(Windows) assert(p.toNativeString() == "test\\"); 360 else assert(p.toNativeString() == "test/"); 361 p.endsWithSlash = false; 362 assert(p.toNativeString() == "test"); 363 } 364 365 unittest 366 { 367 { 368 auto unc = "\\\\server\\share\\path"; 369 auto uncp = NativePath(unc); 370 uncp.normalize(); 371 version(Windows) assert(uncp.toNativeString() == unc); 372 assert(uncp.absolute); 373 assert(!uncp.endsWithSlash); 374 } 375 376 { 377 auto abspath = "/test/path/"; 378 auto abspathp = NativePath(abspath); 379 assert(abspathp.toString() == abspath); 380 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 381 assert(abspathp.absolute); 382 assert(abspathp.endsWithSlash); 383 assert(abspathp.length == 2); 384 assert(abspathp[0] == "test"); 385 assert(abspathp[1] == "path"); 386 } 387 388 { 389 auto relpath = "test/path/"; 390 auto relpathp = NativePath(relpath); 391 assert(relpathp.toString() == relpath); 392 version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); 393 else assert(relpathp.toNativeString() == relpath); 394 assert(!relpathp.absolute); 395 assert(relpathp.endsWithSlash); 396 assert(relpathp.length == 2); 397 assert(relpathp[0] == "test"); 398 assert(relpathp[1] == "path"); 399 } 400 401 { 402 auto winpath = "C:\\windows\\test"; 403 auto winpathp = NativePath(winpath); 404 version(Windows) { 405 assert(winpathp.toString() == "C:/windows/test", winpathp.toString()); 406 assert(winpathp.toNativeString() == winpath); 407 } else { 408 assert(winpathp.toString() == "/C:/windows/test", winpathp.toString()); 409 assert(winpathp.toNativeString() == "/C:/windows/test"); 410 } 411 assert(winpathp.absolute); 412 assert(!winpathp.endsWithSlash); 413 assert(winpathp.length == 3); 414 assert(winpathp[0] == "C:"); 415 assert(winpathp[1] == "windows"); 416 assert(winpathp[2] == "test"); 417 } 418 419 { 420 auto dotpath = "/test/../test2/././x/y"; 421 auto dotpathp = NativePath(dotpath); 422 assert(dotpathp.toString() == "/test/../test2/././x/y"); 423 dotpathp.normalize(); 424 assert(dotpathp.toString() == "/test2/x/y"); 425 } 426 427 { 428 auto dotpath = "/test/..////test2//./x/y"; 429 auto dotpathp = NativePath(dotpath); 430 assert(dotpathp.toString() == "/test/..////test2//./x/y"); 431 dotpathp.normalize(); 432 assert(dotpathp.toString() == "/test2/x/y"); 433 } 434 435 { 436 auto parentpath = "/path/to/parent"; 437 auto parentpathp = NativePath(parentpath); 438 auto subpath = "/path/to/parent/sub/"; 439 auto subpathp = NativePath(subpath); 440 auto subpath_rel = "sub/"; 441 assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); 442 auto subfile = "/path/to/parent/child"; 443 auto subfilep = NativePath(subfile); 444 auto subfile_rel = "child"; 445 assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); 446 } 447 448 { // relative paths across Windows devices are not allowed 449 version (Windows) { 450 auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute); 451 auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute); 452 auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute); 453 auto p4 = NativePath("C:\\somepath"); assert(p4.absolute); 454 auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute); 455 auto p6 = NativePath("D:\\somepath"); assert(p6.absolute); 456 assert(p4.relativeTo(p5) == NativePath("../somepath")); 457 assert(p4.relativeTo(p6) == NativePath("C:\\somepath")); 458 assert(p4.relativeTo(p1) == NativePath("C:\\somepath")); 459 assert(p1.relativeTo(p2) == NativePath("../share")); 460 assert(p1.relativeTo(p3) == NativePath("\\\\server\\share")); 461 assert(p1.relativeTo(p4) == NativePath("\\\\server\\share")); 462 } 463 } 464 } 465 466 unittest { 467 assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz"); 468 assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/"); 469 assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar"); 470 assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/"); 471 assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == ".."); 472 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../"); 473 assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz"); 474 assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/"); 475 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../"); 476 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../"); 477 assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == ""); 478 assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == ""); 479 }