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