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