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