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 { 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 { 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 version(Posix) { if( i > 0 ) ret.put('/'); } 136 else { enforce("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 { return m_nodes.length; } 194 195 /// True if the path contains no entries 196 @property bool empty() const { 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(ref const NativePath rhs) const { 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(const NativePath other) const { 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 pure { return m_name; } 291 292 @property string name() const { return m_name; } 293 294 NativePath opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return NativePath([this, rhs], false); } 295 296 bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } 297 bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } 298 bool opEquals(string rhs) const { return m_name == rhs; } 299 int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } 300 int opCmp(string rhs) const { return m_name.cmp(rhs); } 301 } 302 303 private bool isValidFilename(string str) 304 { 305 foreach( ch; str ) 306 if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; 307 return true; 308 } 309 310 /// Joins two path strings. subpath must be relative. 311 string joinPath(string basepath, string subpath) 312 { 313 NativePath p1 = NativePath(basepath); 314 NativePath p2 = NativePath(subpath); 315 return (p1 ~ p2).toString(); 316 } 317 318 /// Splits up a path string into its elements/folders 319 PathEntry[] splitPath(string path) 320 pure { 321 if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; 322 if( path.empty ) return null; 323 if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; 324 325 // count the number of path nodes 326 size_t nelements = 0; 327 foreach( i, char ch; path ) 328 if( ch == '\\' || ch == '/' ) 329 nelements++; 330 nelements++; 331 332 // reserve space for the elements 333 auto elements = new PathEntry[nelements]; 334 size_t eidx = 0; 335 336 // detect UNC path 337 if(path.startsWith("\\")) 338 { 339 elements[eidx++] = PathEntry(path[0 .. 1]); 340 path = path[1 .. $]; 341 } 342 343 // read and return the elements 344 size_t startidx = 0; 345 foreach( i, char ch; path ) 346 if( ch == '\\' || ch == '/' ){ 347 elements[eidx++] = PathEntry(path[startidx .. i]); 348 startidx = i+1; 349 } 350 elements[eidx++] = PathEntry(path[startidx .. $]); 351 assert(eidx == nelements); 352 return elements; 353 } 354 355 unittest 356 { 357 NativePath p; 358 assert(p.toNativeString() == "."); 359 p.endsWithSlash = true; 360 version(Windows) assert(p.toNativeString() == ".\\"); 361 else assert(p.toNativeString() == "./"); 362 363 p = NativePath("test/"); 364 version(Windows) assert(p.toNativeString() == "test\\"); 365 else assert(p.toNativeString() == "test/"); 366 p.endsWithSlash = false; 367 assert(p.toNativeString() == "test"); 368 } 369 370 unittest 371 { 372 { 373 auto unc = "\\\\server\\share\\path"; 374 auto uncp = NativePath(unc); 375 uncp.normalize(); 376 version(Windows) assert(uncp.toNativeString() == unc); 377 assert(uncp.absolute); 378 assert(!uncp.endsWithSlash); 379 } 380 381 { 382 auto abspath = "/test/path/"; 383 auto abspathp = NativePath(abspath); 384 assert(abspathp.toString() == abspath); 385 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 386 assert(abspathp.absolute); 387 assert(abspathp.endsWithSlash); 388 assert(abspathp.length == 2); 389 assert(abspathp[0] == "test"); 390 assert(abspathp[1] == "path"); 391 } 392 393 { 394 auto relpath = "test/path/"; 395 auto relpathp = NativePath(relpath); 396 assert(relpathp.toString() == relpath); 397 version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); 398 else assert(relpathp.toNativeString() == relpath); 399 assert(!relpathp.absolute); 400 assert(relpathp.endsWithSlash); 401 assert(relpathp.length == 2); 402 assert(relpathp[0] == "test"); 403 assert(relpathp[1] == "path"); 404 } 405 406 { 407 auto winpath = "C:\\windows\\test"; 408 auto winpathp = NativePath(winpath); 409 version(Windows) { 410 assert(winpathp.toString() == "C:/windows/test", winpathp.toString()); 411 assert(winpathp.toNativeString() == winpath); 412 } else { 413 assert(winpathp.toString() == "/C:/windows/test", winpathp.toString()); 414 assert(winpathp.toNativeString() == "/C:/windows/test"); 415 } 416 assert(winpathp.absolute); 417 assert(!winpathp.endsWithSlash); 418 assert(winpathp.length == 3); 419 assert(winpathp[0] == "C:"); 420 assert(winpathp[1] == "windows"); 421 assert(winpathp[2] == "test"); 422 } 423 424 { 425 auto dotpath = "/test/../test2/././x/y"; 426 auto dotpathp = NativePath(dotpath); 427 assert(dotpathp.toString() == "/test/../test2/././x/y"); 428 dotpathp.normalize(); 429 assert(dotpathp.toString() == "/test2/x/y"); 430 } 431 432 { 433 auto dotpath = "/test/..////test2//./x/y"; 434 auto dotpathp = NativePath(dotpath); 435 assert(dotpathp.toString() == "/test/..////test2//./x/y"); 436 dotpathp.normalize(); 437 assert(dotpathp.toString() == "/test2/x/y"); 438 } 439 440 { 441 auto parentpath = "/path/to/parent"; 442 auto parentpathp = NativePath(parentpath); 443 auto subpath = "/path/to/parent/sub/"; 444 auto subpathp = NativePath(subpath); 445 auto subpath_rel = "sub/"; 446 assert(subpathp.relativeTo(parentpathp).toString() == subpath_rel); 447 auto subfile = "/path/to/parent/child"; 448 auto subfilep = NativePath(subfile); 449 auto subfile_rel = "child"; 450 assert(subfilep.relativeTo(parentpathp).toString() == subfile_rel); 451 } 452 453 { // relative paths across Windows devices are not allowed 454 version (Windows) { 455 auto p1 = NativePath("\\\\server\\share"); assert(p1.absolute); 456 auto p2 = NativePath("\\\\server\\othershare"); assert(p2.absolute); 457 auto p3 = NativePath("\\\\otherserver\\share"); assert(p3.absolute); 458 auto p4 = NativePath("C:\\somepath"); assert(p4.absolute); 459 auto p5 = NativePath("C:\\someotherpath"); assert(p5.absolute); 460 auto p6 = NativePath("D:\\somepath"); assert(p6.absolute); 461 assert(p4.relativeTo(p5) == NativePath("../somepath")); 462 assert(p4.relativeTo(p6) == NativePath("C:\\somepath")); 463 assert(p4.relativeTo(p1) == NativePath("C:\\somepath")); 464 assert(p1.relativeTo(p2) == NativePath("../share")); 465 assert(p1.relativeTo(p3) == NativePath("\\\\server\\share")); 466 assert(p1.relativeTo(p4) == NativePath("\\\\server\\share")); 467 } 468 } 469 } 470 471 unittest { 472 assert(NativePath("/foo/bar/baz").relativeTo(NativePath("/foo")).toString == "bar/baz"); 473 assert(NativePath("/foo/bar/baz/").relativeTo(NativePath("/foo")).toString == "bar/baz/"); 474 assert(NativePath("/foo/bar").relativeTo(NativePath("/foo")).toString == "bar"); 475 assert(NativePath("/foo/bar/").relativeTo(NativePath("/foo")).toString == "bar/"); 476 assert(NativePath("/foo").relativeTo(NativePath("/foo/bar")).toString() == ".."); 477 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar")).toString() == "../"); 478 assert(NativePath("/foo/baz").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz"); 479 assert(NativePath("/foo/baz/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../baz/"); 480 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz")).toString() == "../../"); 481 assert(NativePath("/foo/").relativeTo(NativePath("/foo/bar/baz/mumpitz")).toString() == "../../../"); 482 assert(NativePath("/foo").relativeTo(NativePath("/foo")).toString() == ""); 483 assert(NativePath("/foo/").relativeTo(NativePath("/foo")).toString() == ""); 484 }