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