1 // Copyright Ferdinand Majerech 2011 - 2012. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 7 //Normal file system based VFS file/directory implementation. 8 module dgamevfs.fs; 9 10 11 import std.c.stdio; 12 13 import std.algorithm; 14 import std.exception; 15 import std.file; 16 import std.path; 17 import std..string; 18 import std.typecons; 19 20 import dgamevfs.exceptions; 21 import dgamevfs.vfs; 22 import dgamevfs.util; 23 24 25 /** 26 * Directory in the physical filesystem. 27 * 28 * Note that paths behave as in the backend filesystem. 29 * 30 * For example, if you are working with Windows, paths are not case sensitive. 31 */ 32 class FSDir : VFSDir 33 { 34 private: 35 //Path of the directory in the physical filesystem. 36 string physicalPath_; 37 38 //Is this directory writable? 39 Flag!"writable" writable_; 40 41 public: 42 /** 43 * Construct an $(D FSDir). 44 * 45 * Params: name = Name of the directory in the VFS. 46 * physicalPath = Path of the directory in the physical filesystem. 47 * writable = Is this directory _writable? 48 * $(D FSDir) can't determine whether you have permission 49 * to write in a directory - you must specify this 50 * explicitly. 51 */ 52 this(string name, string physicalPath, Flag!"writable" writable = Yes.writable) 53 @safe 54 { 55 this(null, name, physicalPath, writable); 56 } 57 58 override @property bool writable() @safe pure nothrow const @nogc {return writable_;} 59 60 override @property bool exists() @safe const nothrow 61 { 62 try { return .exists(physicalPath_); } 63 catch(Exception e) { assert(false, "std.file.exists() should never throw"); } 64 } 65 66 override VFSFile file(string path) 67 { 68 enforce(isValidPath(path), invalidPath("Invalid physical file path: ", path)); 69 enforce(noPackageSeparators(path), 70 invalidPath("File path contains unexpected package separators: ", path)); 71 enforce(exists, 72 notFound("Trying to access file ", path, " in filesystem directory ", 73 this.path, " with physical path ", physicalPath_, 74 " that does not exist")); 75 76 //Full physical path of the file. 77 const filePath = physicalPath_ ~ "/" ~ path; 78 //Direct parent of requested file - must exist for us to return the file. 79 const dirPath = dirName(filePath); 80 81 enforce(.exists(dirPath), 82 notFound("Trying to access file ", baseName(filePath), 83 " in filesystem directory with physical path ", 84 dirPath, " that does not exist")); 85 enforce(isDir(dirPath), 86 invalidPath("Trying to access file ", baseName(filePath), 87 " in filesystem directory with physical path ", 88 dirPath, " that is not a directory")); 89 90 return new FSFile(this, path, filePath); 91 } 92 93 override VFSDir dir(string path) @trusted 94 { 95 enforce(isValidPath(path), invalidPath("Invalid physical directory path: ", path)); 96 enforce(noPackageSeparators(path), 97 invalidPath("Directory path contains unexpected package separators: ", 98 path)); 99 enforce(exists, 100 notFound("Trying to access directory ", path, " in filesystem directory ", 101 this.path, " with physical path ", physicalPath_, 102 " that does not exist")); 103 104 //Full physical path of the dir. 105 const subdirPath = physicalPath_ ~ "/" ~ path; 106 //Direct parent of requested dir - must exist for us to return the dir. 107 const dirPath = subdirPath.dirName; 108 109 enforce(.exists(dirPath), 110 notFound("Trying to access directory ", subdirPath.baseName, 111 " in filesystem directory with physical path ", dirPath, 112 " that does not exist")); 113 enforce(dirPath.isDir, 114 invalidPath("Trying to access file ", subdirPath.baseName, 115 " in filesystem directory with physical path ", 116 dirPath, " that is not a directory")); 117 118 return new FSDir(this, path, subdirPath, writable_); 119 } 120 121 override VFSFiles files(Flag!"deep" deep = No.deep, string glob = null) @trusted 122 { 123 enforce(exists, 124 notFound("Trying to access files of filesystem directory", path, 125 " with physical path ", physicalPath_, " that does not exist")); 126 127 auto files = new VFSFiles.Items; 128 129 foreach(DirEntry e; dirEntries(physicalPath_, deep ? SpanMode.depth 130 : SpanMode.shallow)) 131 { 132 if(!e.isFile()){continue;} 133 auto relative = e.name; 134 relative.skipOver(physicalPath_); 135 relative.skipOver("/"); 136 relative.skipOver("\\"); 137 if(glob is null || globMatch(relative, glob)) 138 { 139 files.insert(new FSFile(this, relative, e.name)); 140 } 141 } 142 143 return filesRange(files); 144 } 145 146 override VFSDirs dirs(Flag!"deep" deep = No.deep, string glob = null) @trusted 147 { 148 enforce(exists, 149 notFound("Trying to access directories of filesystem directory", path, 150 " with physical path ", physicalPath_, " that does not exist")); 151 152 auto dirs = new VFSDirs.Items; 153 154 foreach(DirEntry e; dirEntries(physicalPath_, deep ? SpanMode.depth 155 : SpanMode.shallow)) 156 { 157 if(!e.isDir()){continue;} 158 auto relative = e.name; 159 relative.skipOver(physicalPath_); 160 relative.skipOver("/"); 161 relative.skipOver("\\"); 162 if(glob is null || globMatch(relative, glob)) 163 { 164 dirs.insert(new FSDir(this, relative, e.name, writable_)); 165 } 166 } 167 168 return dirsRange(dirs); 169 } 170 171 override void remove() 172 { 173 if(!exists){return;} 174 try 175 { 176 rmdirRecurse(physicalPath_); 177 } 178 catch(FileException e) 179 { 180 throw ioError("Failed to remove filesystem directory ", path, 181 " with physical path ", physicalPath_); 182 } 183 } 184 185 protected: 186 override void create_() @trusted 187 { 188 if(exists){return;} 189 try 190 { 191 mkdir(physicalPath_); 192 } 193 catch(FileException e) 194 { 195 throw ioError("Failed to create filesystem directory ", path, 196 " with physical path ", physicalPath_); 197 } 198 } 199 200 override VFSDir copyWithoutParent() 201 { 202 return new FSDir(name, physicalPath_, writable_); 203 } 204 205 private: 206 /* 207 * Construct an FSDir. 208 * 209 * Params: parent = Parent directory. 210 * name = Name of the directory in the VFS. 211 * physicalPath = Path of the directory in the physical filesystem. 212 * writable = Is this directory writable? 213 * FSDir can't determine whether you have permission 214 * to write in a directory - you must specify this 215 * explicitly. 216 */ 217 this(FSDir parent, string pathInParent, string physicalPath, 218 Flag!"writable" writable) 219 @trusted 220 { 221 physicalPath = cleanFSPath(physicalPath); 222 pathInParent = cleanFSPath(pathInParent); 223 enforce(isValidPath(physicalPath), 224 invalidPath("Invalid physical directory path: ", physicalPath)); 225 physicalPath_ = physicalPath; 226 if(exists) 227 { 228 enforce(isDir(physicalPath_), 229 invalidPath("Trying to construct a FSDir with physical path ", 230 physicalPath_, " that is not a directory.")); 231 } 232 writable_ = writable; 233 super(parent, pathInParent); 234 } 235 } 236 237 238 /** 239 * $(D VFSFile) implementation representing a file in the file system. 240 */ 241 class FSFile : VFSFile 242 { 243 private: 244 //File handle when the file is open. 245 FILE* file_ = null; 246 247 //File mode (open, reading, writing, appending). 248 Mode mode_ = Mode.Closed; 249 250 //Path of the file in the physical filesystem. 251 string physicalPath_; 252 253 public: 254 override @property ulong bytes() @trusted const 255 { 256 enforce(exists, 257 notFound("Trying to get size of FSFile ", path, 258 " that does not exist")); 259 try 260 { 261 return getSize(physicalPath_); 262 } 263 catch(FileException e) 264 { 265 throw notFound("Trying to get size of FSFile ", path, 266 " that does not exist"); 267 } 268 } 269 270 override @property bool exists() @safe const nothrow 271 { 272 try { return .exists(physicalPath_); } 273 catch(Exception e) { assert(false, "std.file.exists() should never throw"); } 274 } 275 276 override @property bool open() @safe pure nothrow const @nogc 277 { 278 return mode_ != Mode.Closed; 279 } 280 281 protected: 282 override void openRead() 283 { 284 assert(exists, "Trying to open a nonexistent file for reading: " ~ path); 285 assert(mode_ == Mode.Closed, "Trying to open a file that is already open: " ~ path); 286 287 auto file = fopen(toStringz(physicalPath_), toStringz("rb")); 288 enforce(file !is null, 289 ioError("FSFile ", path, " with physical path ", physicalPath_, 290 " could not be opened for reading")); 291 file_ = file; 292 mode_ = Mode.Read; 293 } 294 295 override void openWrite(Flag!"append" append) 296 { 297 assert(mode_ == Mode.Closed, "Trying to open a file that is already open" ~ path); 298 assert(writable, "Trying open a non-writable file for writing: " ~ path); 299 300 auto file = fopen(toStringz(physicalPath_), 301 toStringz(append ? "ab" : "wb")); 302 enforce(file !is null, 303 ioError("FSFile ", path, " with physical path ", physicalPath_, 304 " could not be opened for writing")); 305 file_ = file; 306 mode_ = (append ? Mode.Append : Mode.Write); 307 } 308 309 override void[] read(void[] target) 310 { 311 assert(mode_ == Mode.Read, 312 "Trying to read from a file not opened for reading: " ~ path); 313 314 return target[0 .. fread(target.ptr, 1, target.length, file_)]; 315 } 316 317 override void write(const void[] data) 318 { 319 assert(mode_ == Mode.Write || mode_ == Mode.Append, 320 "Trying to write to a file not opened for writing/appending: " ~ path); 321 assert(writable, "Trying to write to a non-writable file: " ~ path); 322 323 auto bytesWritten = fwrite(data.ptr, 1, data.length, file_); 324 enforce(bytesWritten == data.length, 325 ioError("Error writing to FSFile ", path, " with physical path ", 326 physicalPath_, " (Possibly out of disk space?).")); 327 enforce(fflush(file_) == 0, 328 ioError("Error writing to FSFile ", path, " with physical path ", 329 physicalPath_)); 330 } 331 332 override void seek(long offset, Seek origin) 333 { 334 assert(mode_ != Mode.Closed, "Trying to seek in an unopened file: " ~ path); 335 336 const length = bytes(); 337 const long base = origin == Seek.Set ? 0 : 338 origin == Seek.Current ? seekPosition() : 339 length; 340 const long position = base + offset; 341 enforce(position >= 0, 342 ioError("Trying to seek before the beginning of file: " ~ path)); 343 enforce(position <= length, 344 ioError("Trying to seek beyond the end of file: " ~ path)); 345 346 static if(size_t.sizeof == 4) 347 { 348 enforce(offset <= int.max, 349 ioError("Seeking beyond 2 GiB not supported on 32bit. File: " ~ path)); 350 const platformOffset = cast(int)offset; 351 } 352 else 353 { 354 alias offset platformOffset; 355 } 356 357 if(fseek(file_, platformOffset, 358 origin == Seek.Set ? SEEK_SET : 359 origin == Seek.Current ? SEEK_CUR : 360 SEEK_END)) 361 { 362 throw ioError("Error seeking in FSFile ", path, " with physical path ", 363 physicalPath_); 364 } 365 } 366 367 override void close() 368 { 369 assert(mode_ != Mode.Closed, "Trying to close an unopened file: " ~ path); 370 371 fclose(file_); 372 file_ = null; 373 mode_ = Mode.Closed; 374 } 375 376 private: 377 /* 378 * Construct an FSFile. 379 * 380 * Params: parent = Parent directory. 381 * pathInParent = Path of the file in the parent directory (aka file name). 382 * physicalPath = Path in the physical filesystem. 383 * 384 * Throws: VFSInvalidPathException if pathInParent is not valid 385 * (contains '/' or "::"). 386 */ 387 this(FSDir parent, string pathInParent, string physicalPath) 388 { 389 physicalPath = cleanFSPath(physicalPath); 390 pathInParent = cleanFSPath(pathInParent); 391 enforce(isValidPath(pathInParent), 392 invalidPath("Invalid file name: ", pathInParent)); 393 physicalPath_ = physicalPath; 394 if(exists) 395 { 396 enforce(isFile(physicalPath_), 397 invalidPath("Trying to construct a FSFile with physical path ", 398 physicalPath_, " that is not a file.")); 399 } 400 super(parent, pathInParent); 401 } 402 403 /* 404 * Construct an FSFile without a parent directory (i.e. outside the VFS). 405 * 406 * Params: physicalPath = Path in the physical filesystem. 407 * 408 * Throws: VFSInvalidPathException if the physical path is not valid. 409 */ 410 this(string physicalPath) 411 { 412 physicalPath = cleanFSPath(physicalPath); 413 enforce(isValidPath(physicalPath), 414 invalidPath("Invalid file name: ", physicalPath)); 415 physicalPath_ = physicalPath; 416 if(exists) 417 { 418 enforce(isFile(physicalPath_), 419 invalidPath("Trying to construct a FSFile with physical path ", 420 physicalPath_, " that is not a file.")); 421 } 422 super(physicalPath); 423 } 424 425 //Determine seek position in the file. 426 @property final ulong seekPosition() 427 { 428 assert(file_ !is null, "Can only get seek position of an open FSFile"); 429 const result = ftell(file_); 430 if(result < 0) 431 { 432 throw ioError("Error determining file position in FSFile ", path, 433 " with physical path ", physicalPath_); 434 } 435 return result; 436 } 437 } 438 439 /// Construct a plain file, not using the VFS. 440 /// 441 /// Allows to use the D:GameVFS API for standalone files. 442 /// 443 /// Params: path = Path of the file in the physical filesystem. 444 /// 445 /// Throws: VFSInvalidPathException if the path is not valid. 446 FSFile physicalFSFile(string path) 447 { 448 return new FSFile(path); 449 }