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 Appender!string ret; 101 102 // for absolute unix paths start with / 103 version(Posix) { if(absolute) ret.put('/'); } 104 105 foreach( i, f; m_nodes ){ 106 version(Windows) { if( i > 0 ) ret.put('\\'); } 107 version(Posix) { if( i > 0 ) ret.put('/'); } 108 else { enforce("Unsupported OS"); } 109 ret.put(f.toString()); 110 } 111 112 if( m_nodes.length > 0 && m_endsWithSlash ){ 113 version(Windows) { ret.put('\\'); } 114 version(Posix) { ret.put('/'); } 115 } 116 117 return ret.data; 118 } 119 120 /// Tests if `rhs` is an anchestor or the same as this path. 121 bool startsWith(const Path rhs) const { 122 if( rhs.m_nodes.length > m_nodes.length ) return false; 123 foreach( i; 0 .. rhs.m_nodes.length ) 124 if( m_nodes[i] != rhs.m_nodes[i] ) 125 return false; 126 return true; 127 } 128 129 /// Computes the relative path from `parentPath` to this path. 130 Path relativeTo(const Path parentPath) const { 131 version(Windows){ 132 // a path such as ..\C:\windows is not valid, so force the path to stay absolute in this case 133 if( this.absolute && !this.empty && m_nodes[0].toString().endsWith(":") && 134 !parentPath.startsWith(this[0 .. 1]) ) 135 { 136 return this; 137 } 138 } 139 int nup = 0; 140 while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ 141 nup++; 142 } 143 Path ret = Path(null, false); 144 ret.m_endsWithSlash = true; 145 foreach( i; 0 .. nup ) ret ~= ".."; 146 ret ~= Path(m_nodes[parentPath.length-nup .. $], false); 147 return ret; 148 } 149 150 /// The last entry of the path 151 @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; } 152 153 /// The parent path 154 @property Path parentPath() const { return this[0 .. length-1]; } 155 156 /// The ist of path entries of which this path is composed 157 @property immutable(PathEntry)[] nodes() const { return m_nodes; } 158 159 /// The number of path entries of which this path is composed 160 @property size_t length() const { return m_nodes.length; } 161 162 /// True if the path contains no entries 163 @property bool empty() const { return m_nodes.length == 0; } 164 165 /// Determines if the path ends with a slash (i.e. is a directory) 166 @property bool endsWithSlash() const { return m_endsWithSlash; } 167 /// ditto 168 @property void endsWithSlash(bool v) { m_endsWithSlash = v; } 169 170 /// Determines if this path goes outside of its base path (i.e. begins with '..'). 171 @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } 172 173 ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } 174 Path opSlice(size_t start, size_t end) const { 175 auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false); 176 if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; 177 return ret; 178 } 179 size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } 180 181 182 Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) { 183 Path ret; 184 ret.m_nodes = m_nodes; 185 ret.m_absolute = m_absolute; 186 ret.m_endsWithSlash = rhs.m_endsWithSlash; 187 ret.normalize(); // needed to avoid "."~".." become "" instead of ".." 188 189 assert(!rhs.absolute, "Trying to append absolute path."); 190 size_t idx = m_nodes.length; 191 foreach(folder; rhs.m_nodes){ 192 switch(folder.toString()){ 193 default: ret.m_nodes = ret.m_nodes ~ folder; break; 194 case ".": break; 195 case "..": 196 enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); 197 if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) 198 ret.m_nodes = ret.m_nodes[0 .. $-1]; 199 else ret.m_nodes = ret.m_nodes ~ folder; 200 break; 201 } 202 } 203 return ret; 204 } 205 206 Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); } 207 Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); } 208 void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); } 209 void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); } 210 void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } 211 212 /// Tests two paths for equality using '=='. 213 bool opEquals(ref const Path rhs) const { 214 if( m_absolute != rhs.m_absolute ) return false; 215 if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; 216 if( m_nodes.length != rhs.length ) return false; 217 foreach( i; 0 .. m_nodes.length ) 218 if( m_nodes[i] != rhs.m_nodes[i] ) 219 return false; 220 return true; 221 } 222 /// ditto 223 bool opEquals(const Path other) const { return opEquals(other); } 224 225 int opCmp(ref const Path rhs) const { 226 if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; 227 foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) 228 if( m_nodes[i] != rhs.m_nodes[i] ) 229 return m_nodes[i].opCmp(rhs.m_nodes[i]); 230 if( m_nodes.length > rhs.m_nodes.length ) return 1; 231 if( m_nodes.length < rhs.m_nodes.length ) return -1; 232 return 0; 233 } 234 } 235 236 struct PathEntry { 237 private { 238 string m_name; 239 } 240 241 this(string str) 242 { 243 assert(str.countUntil('/') < 0 && (str.countUntil('\\') < 0 || str.length == 1)); 244 m_name = str; 245 } 246 247 string toString() const { return m_name; } 248 249 Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); } 250 251 bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } 252 bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } 253 bool opEquals(string rhs) const { return m_name == rhs; } 254 int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } 255 int opCmp(string rhs) const { return m_name.cmp(rhs); } 256 } 257 258 private bool isValidFilename(string str) 259 { 260 foreach( ch; str ) 261 if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; 262 return true; 263 } 264 265 /// Joins two path strings. subpath must be relative. 266 string joinPath(string basepath, string subpath) 267 { 268 Path p1 = Path(basepath); 269 Path p2 = Path(subpath); 270 return (p1 ~ p2).toString(); 271 } 272 273 /// Splits up a path string into its elements/folders 274 PathEntry[] splitPath(string path) 275 { 276 if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; 277 if( path.empty ) return null; 278 if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; 279 280 // count the number of path nodes 281 size_t nelements = 0; 282 foreach( i, char ch; path ) 283 if( ch == '\\' || ch == '/' ) 284 nelements++; 285 nelements++; 286 287 // reserve space for the elements 288 auto elements = new PathEntry[nelements]; 289 size_t eidx = 0; 290 291 // detect UNC path 292 if(path.startsWith("\\")) 293 { 294 elements[eidx++] = PathEntry(path[0 .. 1]); 295 path = path[1 .. $]; 296 } 297 298 // read and return the elements 299 size_t startidx = 0; 300 foreach( i, char ch; path ) 301 if( ch == '\\' || ch == '/' ){ 302 enforce(i - startidx > 0, "Empty path entries not allowed."); 303 elements[eidx++] = PathEntry(path[startidx .. i]); 304 startidx = i+1; 305 } 306 elements[eidx++] = PathEntry(path[startidx .. $]); 307 enforce(path.length - startidx > 0, "Empty path entries not allowed."); 308 assert(eidx == nelements); 309 return elements; 310 } 311 312 unittest 313 { 314 { 315 auto unc = "\\\\server\\share\\path"; 316 auto uncp = Path(unc); 317 version(Windows) assert(uncp.toNativeString() == unc); 318 assert(uncp.absolute); 319 assert(!uncp.endsWithSlash); 320 } 321 322 { 323 auto abspath = "/test/path/"; 324 auto abspathp = Path(abspath); 325 assert(abspathp.toString() == abspath); 326 version(Windows) {} else assert(abspathp.toNativeString() == abspath); 327 assert(abspathp.absolute); 328 assert(abspathp.endsWithSlash); 329 assert(abspathp.length == 2); 330 assert(abspathp[0] == "test"); 331 assert(abspathp[1] == "path"); 332 } 333 334 { 335 auto relpath = "test/path/"; 336 auto relpathp = Path(relpath); 337 assert(relpathp.toString() == relpath); 338 version(Windows) assert(relpathp.toNativeString() == "test\\path\\"); 339 else assert(relpathp.toNativeString() == relpath); 340 assert(!relpathp.absolute); 341 assert(relpathp.endsWithSlash); 342 assert(relpathp.length == 2); 343 assert(relpathp[0] == "test"); 344 assert(relpathp[1] == "path"); 345 } 346 347 { 348 auto winpath = "C:\\windows\\test"; 349 auto winpathp = Path(winpath); 350 assert(winpathp.toString() == "/C:/windows/test"); 351 version(Windows) assert(winpathp.toNativeString() == winpath); 352 else assert(winpathp.toNativeString() == "/C:/windows/test"); 353 assert(winpathp.absolute); 354 assert(!winpathp.endsWithSlash); 355 assert(winpathp.length == 3); 356 assert(winpathp[0] == "C:"); 357 assert(winpathp[1] == "windows"); 358 assert(winpathp[2] == "test"); 359 } 360 361 { 362 auto dotpath = "/test/../test2/././x/y"; 363 auto dotpathp = Path(dotpath); 364 assert(dotpathp.toString() == "/test/../test2/././x/y"); 365 dotpathp.normalize(); 366 assert(dotpathp.toString() == "/test2/x/y"); 367 } 368 }