1 module jaster.stream.file;
2 
3 private
4 {
5     import std.stdio     : File, SEEK_CUR, SEEK_END, SEEK_SET;
6     import std.file      : exists, fwrite = write;
7     import std.exception : enforce;
8     import std.typecons  : Flag;
9     import jaster.stream;
10 }
11 
12 /++
13  + This is a `Stream` that writes/reads directly from, and to a file.
14  +
15  + It uses `std.stdio.File` internally.
16  +
17  + Other than exceptions, this class does not allocate any GC memory (though the same cannot be said for `File`,
18  + I haven't checked yet).
19  + ++/ 
20 class FileStream : Stream
21 {
22     alias DisposeOnDtor = Flag!"doDtor";
23 
24     /// Describes how the file should be opened.
25     ///
26     /// This will also determine the capabilities (`Stream.Can`) of the stream.
27     enum Mode : int
28     {
29         /// Opens the $(B only) if it exists.
30         Open     = 1 << 0,
31 
32         /// Creates the file if it doesn't exist, and then acts the same as `Mode.Open`.
33         Create   = 1 << 1,
34 
35         /// Supports reading.
36         Read     = 1 << 2,
37         
38         /// Supports writing.
39         Write    = 1 << 3,
40 
41         /// Opens the file in binary mode.
42         Binary   = 1 << 4,
43 
44         /// Sets the file's size to 0 upon opening. $(B Does not imply `Mode.Create`)
45         ///
46         /// Combine with Mode.Create to always make sure to either create, or truncate a file, meaning it's always there
47         /// and always empty.
48         Truncate = 1 << 5,
49 
50         ReadWrite             = Read         | Write,
51         OpenOrCreate          = Open         | Create,
52         ReadOpen              = Open         | Read,
53         ReadCreate            = Create       | Read,
54         ReadOpenOrCreate      = OpenOrCreate | Read,
55         WriteOpen             = Open         | Write,
56         WriteCreate           = Create       | Write,
57         WriteOpenOrCreate     = OpenOrCreate | Write,
58         ReadWriteOpen         = Open         | Read  | Write,
59         ReadWriteCreate       = Create       | Read  | Write,
60         ReadWriteOpenOrCreate = OpenOrCreate | Read  | Write
61     }
62 
63     private
64     {
65         File            _file;
66         Mode            _mode;
67         DisposeOnDtor   _doDtor;
68 
69         static const ubyte[] EMPTY_DATA = [];
70     }
71 
72     /++
73      + Opens the specified file.
74      +
75      + Notes:
76      +  During destruction of a stream made from this constructor, the `dispose` function will
77      +  be called if the object is not already disposed.
78      +
79      + Throws:
80      +  `StreamException` if `file` could not be found.
81      +
82      +  `StreamException` if `mode` doesn't specify either `Open`, `Create` in some form.
83      +
84      + Params:
85      +  mode = The `FileStream.Mode` to open the `file` in.
86      +  file = The file to open.
87      + ++/
88     this(Mode mode, const char[] file)
89     {
90         if((mode & Mode.Create) && !file.exists)
91             fwrite(file, EMPTY_DATA);
92 
93         if(mode & Mode.Open)
94             enforce!StreamException(file.exists, "File not found: "~file);
95         else if(!(mode & Mode.Create))
96             throw new StreamException("The given mode does not specify Create, Open, or OpenOrCreate.");
97 
98         char[3] buffer;
99         this._file   = File(file, mode.modeToString(buffer));
100         this._mode   = mode;
101         this._doDtor = DisposeOnDtor.yes;
102 
103         super(mode.canFromMode());        
104     }
105 
106     /++
107      + Wraps around an existing `File`.
108      +
109      + Assertions:
110      +  `file` must have a file already opened.
111      +
112      + Notes:
113      +  Unlike the other constructor, objects made from this constructor will only call `dispose`
114      +  during the dtor if `doDtor` is set to `DisposeOnDtor.yes`.
115      +
116      +  This means, that if either this stream is disposed manually, or this object is collected/destroyed,
117      +  then external copies of the `file` will also end up being closed. Which could possibly lead to confusion.
118      +
119      +  Also note that due to how this stream's `isDispose` works, if `file` is closed outside of this stream, then
120      +  this stream will begin to throw `StreamDisposedException`s.
121      +
122      + Params:
123      +  mode   = The `FileStream.Mode` to use. For this constructor, `Open`, `Create`, and `Truncate` have no effect.
124      +  file   = The `File` to wrap around.
125      +  doDtor = See the `Notes` section.
126      + ++/
127     this(Mode mode, File file, DisposeOnDtor doDtor = DisposeOnDtor.yes)
128     {
129         assert(file.isOpen, "Make sure the file is actually open first.");
130 
131         this._file   = file;
132         this._mode   = mode;
133         this._doDtor = doDtor;
134         
135         super(mode.canFromMode());
136     }
137 
138     ~this()
139     {
140         if(this._doDtor)
141             this.dispose();
142     }
143 
144     public override
145     {	
146         const(ubyte[]) write(scope const ubyte[] data) 
147         {
148             enforceNotDisposed(this);
149             enforceCanWrite(this);
150             this._file.rawWrite(data);
151 
152             return data; // rawWrite throws an exception if the entire thing isn't written... Maybe I should wrap it around a StreamException
153         }
154         
155         ubyte[] readToBuffer(scope ref ubyte[] buffer) 
156         {
157             enforceNotDisposed(this);
158             enforceCanRead(this);
159 
160             return this._file.rawRead(buffer);
161         }
162         
163         void dispose() 
164         {
165             if(!this.isDisposed)
166                 this.flush();
167 
168             this._file.close();
169         }
170         
171         void flush() 
172         {
173             enforceNotDisposed(this);
174             this._file.flush();
175             this._file.sync();
176         }
177         
178         void seek(SeekFrom from, ptrdiff_t amount) 
179         {
180             enforceNotDisposed(this);
181             auto start = (from == SeekFrom.Start)
182                          ? SEEK_SET
183                          : (from == SeekFrom.Current)
184                             ? SEEK_CUR
185                             : SEEK_END;
186             
187             auto oldPos = this.position;
188             this._file.seek(cast(long)amount, start);
189 
190             if(this.position > this.length)
191             {
192                 import std.format : format;
193                 auto debugPos = this.position;
194                 this.position = oldPos;
195                 throw new StreamException(
196                     format("Attempted to seek past the stream. Destination = %s. Length = %s",
197                         debugPos,
198                         this.length
199                     )
200                 );
201             }
202         }
203         
204         /// For FileStreams, this function will return `true` if the underlying `File` has been closed.
205         @property
206         bool isDisposed() const
207         {
208             return !this._file.isOpen;
209         }
210 
211         @property
212         void writeTimeout(Duration dur)
213         {
214             enforceNotDisposed(this);
215             throw new StreamCannotTimeoutException();
216         }
217 
218         @property
219         Duration writeTimeout()
220         {
221             enforceNotDisposed(this);
222             throw new StreamCannotTimeoutException();
223         }
224 
225         @property
226         void readTimeout(Duration dur)
227         {
228             enforceNotDisposed(this);
229             throw new StreamCannotTimeoutException();
230         }
231 
232         @property
233         Duration readTimeout()
234         {
235             enforceNotDisposed(this);
236             throw new StreamCannotTimeoutException();
237         }
238         
239         @property 
240         void length(size_t size) 
241         {
242             if(size > this.length)
243             {
244                 auto amount = (size - this.length);
245                 auto pos    = this.position;
246                 scope(exit) this.position = pos;
247 
248                 this._file.seek(size - 1, SEEK_SET);
249 
250                 byte b = 0;
251                 this._file.rawWrite((&b)[0..1]);
252             }
253         }
254         
255         @property 
256         size_t length() 
257         {
258             enforceNotDisposed(this);
259 
260             auto size = this._file.size();
261             enforce!StreamException(size <= size_t.max, "The size is out of bounds of size_t.max");
262             return cast(size_t)size;
263         }
264         
265         @property 
266         void position(size_t pos) 
267         {
268             this.seek(SeekFrom.Start, pos);
269         }
270         
271         @property 
272         size_t position()
273         {
274             enforceNotDisposed(this);
275 
276             auto pos = this._file.tell();
277             enforce!StreamException(pos <= size_t.max, "The position is out of bounds of size_t.max");
278             return cast(size_t)pos;
279         }
280     }    
281 }
282 ///
283 unittest
284 {
285     import std.exception : assertThrown;
286 
287     auto mode   = FileStream.Mode.ReadWriteCreate | FileStream.Mode.Truncate | FileStream.Mode.Binary;
288     auto stream = new FileStream(mode, "tests/fs2.bin");
289 
290     stream.length = 200;
291     assert(stream.length == 200);
292 
293     assertThrown!StreamException(stream.position = 201);
294 }
295 
296 // Stream.Can checks
297 unittest
298 {
299     import std.exception : assertThrown;
300 
301     assert( new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin").canWrite);
302     assert( new FileStream(FileStream.Mode.ReadCreate,  "tests/fs1.bin").canRead);
303     assert(!new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin").canRead);
304 
305     assertThrown!StreamCannotWriteException(new FileStream(FileStream.Mode.ReadOpen, "tests/fs1.bin").write([1]));
306     assertThrown!StreamCannotReadException(new FileStream(FileStream.Mode.WriteOpen, "tests/fs1.bin").read(1));
307 }
308 
309 // Dispose checks
310 unittest
311 {
312     import std.exception : assertThrown;
313 
314     auto stream = new FileStream(FileStream.Mode.WriteCreate, "tests/fs1.bin");
315     assert(!stream.isDisposed);
316     stream.dispose();
317     assert(stream.isDisposed);
318 
319     assertThrown!StreamDisposedException(stream.write([1]));
320 }
321 
322 /// Returns: A `Stream.Can` from a `FileStream.Mode`.
323 Stream.Can canFromMode(FileStream.Mode mode)
324 {
325     Stream.Can can = Stream.Can.None;
326 
327          if(mode & FileStream.Mode.Write) can |= Stream.Can.Write;
328     else if(mode & FileStream.Mode.Read)  can |= Stream.Can.Read;
329 
330     return can;
331 }
332 
333 private char[] modeToString(FileStream.Mode mode, return out char[3] str)
334 {
335     size_t plusIndex = 1;
336     
337     if(mode & FileStream.Mode.Binary)
338     {
339         str[1]    = 'b';
340         plusIndex = 2;
341     }
342 
343     if(mode & FileStream.Mode.ReadWrite)
344         str[plusIndex] = '+';
345 
346     // Because C is awkward, we'll just make the file ourself if it doesn't exist.
347     // Being able to open a file, with write-only access, *without* truncating the file, seems impossible.
348     str[0] = (mode & FileStream.Mode.Truncate) ? 'w' : 'r';
349 
350     return str[0..plusIndex+1];
351 }