1 module jaster.stream.core;
2 
3 private
4 {
5     import std.experimental.allocator : makeArray, dispose;
6     import std.exception              : basicExceptionCtors, enforce;
7     import jaster.stream.util;
8 }
9 
10 public import core.time : Duration;
11 
12 /++
13  + The base class for all streams.
14  +
15  + Streams are a common interface for reading/writing a 'stream' of bytes.
16  +
17  + Other classes can be built on top of streams, such as the `BinaryIO` class to provide more
18  + specific functionality to streams.
19  + ++/
20 abstract class Stream
21 {
22     /// A flag enum specifying what the stream is capable of doing.
23     enum Can
24     {
25         None = 0,
26 
27         /// The stream supports writing.
28         Write   = 1 << 0,
29 
30         /// The stream supports reading.
31         Read    = 1 << 1,
32 
33         /// The stream supports seeking.
34         Seek    = 1 << 2,
35 
36         /// The stream supports being able to set timeouts.
37         Timeout = 1 << 3
38     }
39 
40     /// Used with `seek`.
41     enum SeekFrom
42     {
43         /// Seek from the start.
44         Start,
45 
46         /// Seek from the current position.
47         Current,
48 
49         /++
50          + Seek from the end.
51          +
52          + Implementation_Note:
53          +  Some might think that when using `End`, that the amount to seek by means 'How many spaces to go backwards'.
54          +
55          +  This however, is not how I feel it should work. So if you want to go backwards from the end, just use a negative offset,
56          +  like with the other options.
57          + ++/
58         End
59     }
60 
61     private
62     {
63         Can _can;
64     }
65 
66     ///
67     @safe @nogc
68     this(Can can) nothrow pure
69     {
70         this._can = can;
71     }
72 
73     // ######################
74     // # ABSTRACT FUNCTIONS #
75     // ######################
76     public abstract
77     {
78         /++
79          + Writes bytes into the stream.
80          +
81          + Params:
82          +  data = The data to write.
83          +
84          + Throws:
85          +  `StreamCannotWriteException` if this stream does not support writing.
86          +  `StreamDisposedException` if this stream has been disposed of.
87          +
88          + Returns:
89          +  A slice of `data`, specifying which portion of it has been written to the stream.
90          + ++/
91         const(ubyte[]) write(scope const ubyte[] data);
92 
93         /++
94          + Reads bytes from the stream into a buffer.
95          +
96          + Params:
97          +  buffer = The buffer to read into, the length of the buffer determines how many bytes to read.
98          +
99          + Throws:
100          +  `StreamCannotReadException` if this stream does not support reading.
101          +  `StreamDisposedException` if this stream has been disposed of.
102          +
103          + Returns:
104          +  A slice of `buffer`, specifying which portion of it has been filled with data.
105          +  The length of this slice is also how to check how many bytes were read in.
106          + ++/
107         ubyte[] readToBuffer(scope ref ubyte[] buffer);
108 
109         /++
110          + Disposes of the stream's resources.
111          +
112          + If the stream has already been disposed of, this function does nothing.
113          +
114          + Once disposed of, the stream should cease to function, and should throw `StreamDisposedException`s where appropriate.
115          + ++/
116         void dispose();
117 
118         /++
119          + Flushes the stream's data.
120          +
121          + Some streams don't support flushing, so this function will do nothing in those cases.
122          + ++/
123         void flush();
124 
125         /++
126          + Seeks somewhere into the stream.
127          +
128          + Throws:
129          +  `StreamCannotSeekException` if this stream does not support seeking.
130          +  `StreamDisposedException` if this stream has been disposed of.
131          +
132          + Params:
133          +  from   = The 'origin' from where to seek from.
134          +  amount = The amount to seek by. Note that negative numbers can be used to go backwards.
135          + ++/
136         void seek(SeekFrom from, ptrdiff_t amount);
137 
138         /// Returns: If the stream has been disposed of or not.
139         @property
140         bool isDisposed() const;
141 
142         /++
143          + Sets the write timeout for the stream.
144          +
145          + Throws:
146          +  `StreamCannotTimeoutException` if the stream does not support timeouts.
147          +  `StreamDisposedException` if the stream has been disposed of.
148          +
149          + Params:
150          +  dur = The duration to set the timeout for.
151          + ++/
152         @property
153         void writeTimeout(Duration dur);
154 
155         /++
156          + Gets the write timeout for the stream.
157          +
158          + Throws:
159          +  `StreamCannotTimeoutException` if the stream does not support timeouts.
160          +  `StreamDisposedException` if the stream has been disposed of.
161          +
162          + Returns:
163          +  The duration to set the timeout for.
164          + ++/
165         @property
166         Duration writeTimeout();
167 
168         /++
169          + Sets the read timeout for the stream.
170          +
171          + Throws:
172          +  `StreamCannotTimeoutException` if the stream does not support timeouts.
173          +  `StreamDisposedException` if the stream has been disposed of.
174          +
175          + Params:
176          +  dur = The duration to set the timeout for.
177          + ++/
178         @property
179         void readTimeout(Duration dur);
180 
181         /++
182          + Gets the read timeout for the stream.
183          +
184          + Throws:
185          +  `StreamCannotTimeoutException` if the stream does not support timeouts.
186          +  `StreamDisposedException` if the stream has been disposed of.
187          +
188          + Returns:
189          +  The duration to set the timeout for.
190          + ++/
191         @property
192         Duration readTimeout();
193 
194         /++
195          + Sets the length of the stream's data.
196          +
197          + Throws:
198          +  `StreamException` if the stream does not support this function.
199          +  `StreamDisposedException` if the stream has been disposed of.
200          +
201          + Params:
202          +  size = The size to set the stream's length to.
203          + ++/
204         @property
205         void length(size_t size);
206 
207         /++
208          + Gets the length of the stream's data.
209          +
210          + Throws:
211          +  `StreamException` if the stream does not support this function.
212          +  `StreamDisposedException` if the stream has been disposed of.
213          +
214          + Returns:
215          +  The size to set the stream's length.
216          + ++/
217         @property
218         size_t length();
219 
220         /++
221          + Sets the position of the stream's 'cursor'.
222          +
223          + Throws:
224          +  See `seek`.
225          +
226          + Params:
227          +  pos = The position to use.
228          + ++/
229         @property
230         void position(size_t pos);
231 
232         /++
233          + Gets the position of the stream's 'cursor'.
234          +
235          + Throws:
236          +  See `seek`.
237          +
238          + Returns:
239          +  The position to use.
240          + ++/
241         @property
242         size_t position();
243     }
244 
245     // #####################
246     // # VIRTUAL FUNCTIONS #
247     // #####################
248     public
249     {
250         /++
251          + Creates a GC-allocated buffer with a specified size, and attempts to read in a certain amount of bytes.
252          +
253          + Params:
254          +  amount = The amount of bytes to read in.
255          +
256          + Returns:
257          +  The portion of the buffer that was filled by `readToBuffer`.
258          + ++/
259         ubyte[] read(size_t amount)
260         {
261             auto array = new ubyte[amount];
262             return this.readToBuffer(array);
263         }
264 
265         /++
266          + Copies data $(B from the current position in this stream) to the end of this stream, into
267          + the given stream. $(B The given stream is written to using it's current position as well).
268          +
269          + Params:
270          +  to      = The `Stream` to copy the data to.
271          +  buffer  = The buffer used to temporarily store the data as it's being copied over.
272          +            This buffer also determines how many bytes are copied at a time.
273          + ++/
274         void copyTo(Stream to, scope ubyte[] buffer)
275         {
276             // Slight optimisation. Only useful for when the buffer is a small size relative to how much is being copied.
277             // It also depends on the stream itself whether it has any real impact or not.
278             if(this.canSeek && to.canSeek)
279             {
280                 auto end = to.position + (this.length - this.position);
281                 if(end > to.length)
282                     to.length = end;
283             }
284 
285             while(true)
286             {
287                 auto slice = this.readToBuffer(buffer);
288                 if(slice.length == 0)
289                     break;
290 
291                 to.write(slice);
292             }
293         }
294     }
295 
296     // ##############################
297     // # FINAL FUNCTIONS/PROPERTIES #
298     // ##############################
299     public final
300     {
301         /// Shortcut for `seek(SeekFrom.Start)`
302         void seekStart(ptrdiff_t amount)
303         {
304             this.seek(SeekFrom.Start, amount);
305         }
306 
307         /// Shortcut for `seek(SeekFrom.Current)`
308         void seekCurr(ptrdiff_t amount)
309         {
310             this.seek(SeekFrom.Current, amount);
311         }
312 
313         /// Shortcut for `seek(SeekFrom.End)`
314         void seekEnd(ptrdiff_t amount)
315         {
316             this.seek(SeekFrom.End, amount);
317         }
318 
319         /++
320          + A helper function for `copyTo`, that uses a buffer placed on the stack.
321          +
322          + Params:
323          +  BufferSize = The size to give the buffer.
324          +  to         = The stream to copy the data to.
325          + ++/
326         void copyToStack(size_t BufferSize)(Stream to)
327         if(BufferSize > 0)
328         {
329             ubyte[BufferSize] buffer;
330             this.copyTo(to, buffer[]);
331         }
332 
333         /++
334          + A helper function for `copyTo`, that uses a buffer created from an `std.experimental.allocator` Allocator.
335          + The buffer is of course disposed of afterwards.
336          +
337          + Params:
338          +  Alloc       = The allocator to use.
339          +  to          = The stream to copy the data to.
340          +  bufferSize  = The size to give the buffer.
341          +  alloc       = [Only for non-static allocator] the allocator instance to use.
342          + ++/
343         void copyToAlloc(Alloc)(Stream to, size_t bufferSize)
344         if(AllocHelper!Alloc.IsStatic)
345         {
346             assert(bufferSize > 0, "Buffer size cannot be 0");
347             ubyte[] buffer = Alloc.instance.makeArray!ubyte(bufferSize);
348             scope(exit) Alloc.instance.dispose(buffer);
349 
350             this.copyTo(to, buffer);
351         }
352 
353         /// ditto.
354         void copyToAlloc(Alloc)(Stream to, size_t bufferSize, auto ref Alloc alloc)
355         if(!AllocHelper!Alloc.IsStatic)
356         {
357             assert(bufferSize > 0, "Buffer size cannot be 0");
358             ubyte[] buffer = alloc.makeArray!ubyte(bufferSize);
359             scope(exit) alloc.dispose(buffer);
360 
361             this.copyTo(to, buffer);
362         }
363 
364         /++
365          + A helper function for `copyTo`, that uses a buffer allocated by the GC.
366          +
367          + Params:
368          +  to         = The stream to copy the data to.
369          +  bufferSize = The size to give the buffer.
370          + ++/
371         void copyToGC(Stream to, size_t bufferSize)
372         {
373             assert(bufferSize > 0, "Buffer size cannot be 0");
374             ubyte[] buffer = new ubyte[bufferSize];
375 
376             this.copyTo(to, buffer);
377         }
378 
379         /// Returns: What the stream is capable of.
380         @property @safe @nogc
381         Can can() nothrow const
382         {
383             return this._can;
384         }
385 
386         /// Returns: Whether the stream can write.
387         @property @safe @nogc
388         bool canWrite() nothrow const
389         {
390             return (this.can & Can.Write) > 0;
391         }
392 
393         /// Returns: Whether the stream can Read.
394         @property @safe @nogc
395         bool canRead() nothrow const
396         {
397             return (this.can & Can.Read) > 0;
398         }
399 
400         /// Returns: Whether the stream can Seek.
401         @property @safe @nogc
402         bool canSeek() nothrow const
403         {
404             return (this.can & Can.Seek) > 0;
405         }
406 
407         /// Returns: Whether the stream can Timeout.
408         @property @safe @nogc
409         bool canTimeout() nothrow const
410         {
411             return (this.can & Can.Timeout) > 0;
412         }
413     }
414 }
415 
416 /++
417  + Throws an exception if the given stream does not support writing/reading/seeking/timeouts.
418  + ++/
419 void enforceCanWrite(const Stream stream)
420 {
421     enforce!StreamCannotWriteException(stream.canWrite);
422 }
423 
424 /// ditto
425 void enforceCanRead(const Stream stream)
426 {
427     enforce!StreamCannotReadException(stream.canRead);
428 }
429 
430 /// ditto
431 void enforceCanSeek(const Stream stream)
432 {
433     enforce!StreamCannotSeekException(stream.canSeek);
434 }
435 
436 /// ditto
437 void enforceCanTimeout(const Stream stream)
438 {
439     enforce!StreamCannotTimeoutException(stream.canTimeout);
440 }
441 
442 /// ditto
443 void enforceNotDisposed(const Stream stream)
444 {
445     enforce!StreamDisposedException(!stream.isDisposed);
446 }
447 
448 /// Base stream exception class.
449 class StreamException : Exception
450 {
451     mixin basicExceptionCtors;
452 }
453 
454 /// Thrown if the stream doesn't support writing.
455 class StreamCannotWriteException : StreamException
456 {
457     mixin basicExceptionCtors;
458 
459     this(string file = __FILE__, size_t line = __LINE__, Throwable next = null)
460     {
461         super("This stream does not support writing/is read only.", file, line, next);
462     }
463 }
464 
465 /// ditto
466 class StreamCannotReadException : StreamException
467 {
468     mixin basicExceptionCtors;
469 
470     this(string file = __FILE__, size_t line = __LINE__, Throwable next = null)
471     {
472         super("This stream does not support reading.", file, line, next);
473     }
474 }
475 
476 /// ditto
477 class StreamCannotSeekException : StreamException
478 {
479     mixin basicExceptionCtors;
480 
481     this(string file = __FILE__, size_t line = __LINE__, Throwable next = null)
482     {
483         super("This stream does not support seeking.", file, line, next);
484     }
485 }
486 
487 /// ditto
488 class StreamCannotTimeoutException : StreamException
489 {
490     mixin basicExceptionCtors;
491 
492     this(string file = __FILE__, size_t line = __LINE__, Throwable next = null)
493     {
494         super("This stream does not support timing out.", file, line, next);
495     }
496 }
497 
498 /// ditto
499 class StreamDisposedException : StreamException
500 {
501     mixin basicExceptionCtors;
502 
503     this(string file = __FILE__, size_t line = __LINE__, Throwable next = null)
504     {
505         super("This stream has been disposed.", file, line, next);
506     }
507 }