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 }