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