1 module jaster.stream.range;
2 
3 private
4 {
5     import std.range                  : ElementType, isInputRange;
6     import std.typecons               : RefCounted, refCounted, RefCountedAutoInitialize;
7     import std.experimental.allocator : dispose, makeArray;
8     import jaster.stream.core, jaster.stream.util;
9 }
10 
11 /// See `byChunkAlloc`.
12 struct ByChunkAllocRange(Alloc)
13 {
14     private static struct Payload
15     {
16         alias _buffer this;
17         private ubyte[]           _buffer;
18         private AllocHelper!Alloc _alloc;
19         ~this()
20         {
21             if(this._buffer !is null)
22                 this._alloc.alloc.dispose(this._buffer);
23         }
24     }
25 
26     alias RC = RefCounted!(Payload, RefCountedAutoInitialize.no);
27     private RC      _buffer;
28     private Stream  _stream;
29     private ubyte[] _used;
30     private bool    _empty;
31 
32     static assert(isInputRange!(typeof(this)));
33     static assert(is(ElementType!(typeof(this)) == ubyte[]));
34 
35     this(ubyte[] buffer, Stream stream, AllocHelper!Alloc alloc)
36     {
37         this._buffer = refCounted(Payload(buffer, alloc));
38         this._stream = stream;
39     }
40 
41     void popFront()
42     {
43         this._used = this._stream.readToBuffer(this._buffer);
44         if(this._used.length == 0)
45             this._empty = true;
46     }
47 
48     @nogc
49     ubyte[] front() nothrow
50     {
51         return this._used;
52     }
53 
54     @safe @nogc
55     bool empty() nothrow pure const
56     {
57         return this._empty;
58     }
59 }
60 
61 /// See `byChunkGC`
62 struct ByChunkGCRange
63 {
64     private Stream  _stream;
65     private ubyte[] _buffer;
66     private ubyte[] _used;
67     private bool    _empty;
68 
69     static assert(isInputRange!(typeof(this)));
70     static assert(is(ElementType!(typeof(this)) == ubyte[]));
71 
72     this(ubyte[] buffer, Stream stream)
73     {
74         this._buffer = buffer;
75         this._stream = stream;
76     }
77 
78     void popFront()
79     {
80         this._used = this._stream.readToBuffer(this._buffer);
81         if(this._used.length == 0)
82             this._empty = true;
83     }
84 
85     @nogc
86     ubyte[] front() nothrow
87     {
88         return this._used;
89     }
90 
91     @safe @nogc
92     bool empty() nothrow pure const
93     {
94         return this._empty;
95     }
96 }
97 
98 /++
99  + Allows access to a stream's data as an input range, chunk-by-chunk.
100  +
101  + The resulting range is only empty once the stream has no more data to read.
102  +
103  + Notes:
104  +  The stream's $(B current) position will be used.
105  +
106  +  No part of this function or the resulting range uses GC memory. GC memory is only
107  +  used if `Alloc` uses the GC, and/or if the given `stream` uses the GC.
108  +
109  +  The resulting range uses `std.typecons.RefCounted` for it's internal buffer, so
110  +  copying the range has a small overhead.
111  +
112  + Params:
113  +  stream      = The `Stream` to use.
114  +  bufferSize  = The size of the buffer, this also determines the max size of each chunk.
115  +  alloc       = [Non-static allocators only] The allocator instance to use.
116  + ++/
117 ByChunkAllocRange!Alloc byChunkAlloc(Alloc)(Stream stream, size_t bufferSize)
118 if(AllocHelper!Alloc.IsStatic)
119 {
120     auto r = ByChunkAllocRange!Alloc(Alloc.instance.makeArray!ubyte(bufferSize), stream, AllocHelper!Alloc());
121     r.popFront();
122     return r;
123 }
124 ///
125 unittest
126 {
127     import std.experimental.allocator.mallocator : Mallocator;
128     import std.algorithm                         : all;
129     import jaster.stream.memory                  : MemoryStreamGC;
130 
131     auto stream = new MemoryStreamGC();
132     foreach(i; 0..129)
133         stream.write([cast(ubyte)i]);
134 
135     stream.position = 0;
136     assert(stream.byChunkAlloc!Mallocator(4).all!(arr => arr.length > 0));
137 }
138 
139 /// ditto.
140 ByChunkAllocRange!Alloc byChunkAlloc(Alloc)(Stream stream, size_t bufferSize, Alloc alloc)
141 {
142     auto r = ByChunkAllocRange!Alloc(alloc.makeArray!ubyte(bufferSize), stream, AllocHelper!Alloc(alloc));
143     r.popFront();
144     return r;
145 }
146 ///
147 unittest
148 {
149     import std.experimental.allocator.building_blocks : StatsCollector, Options;
150     import std.experimental.allocator.mallocator      : Mallocator;
151     import std.algorithm                              : all;
152     import jaster.stream.memory                       : MemoryStreamGC;
153 
154     alias Alloc = StatsCollector!(Mallocator, Options.all, Options.all);
155     auto alloc  = new Alloc();
156 
157     auto stream = new MemoryStreamGC();
158     foreach(i; 0..129)
159         stream.write([cast(ubyte)i]);
160 
161     stream.position = 0;
162     assert(stream.byChunkAlloc!(Alloc*)(4, alloc).all!(arr => arr.length > 0));
163     assert(alloc.bytesUsed == 0);
164 
165     // TODO: Test with more complex range chains, including holding the resulting range in a variable and doing stuff with it manually.
166     //       If that all passes with 0 bytes used in the end, and 0 crashes, then it should all be good :)
167 }
168 
169 /// Variation of `byChunkAlloc` that uses a GC allocated array, and doesn't use ref counting.
170 ByChunkGCRange byChunkGC(Stream stream, size_t bufferSize)
171 {
172     auto r = ByChunkGCRange(new ubyte[bufferSize], stream);
173     r.popFront();
174     return r;
175 }
176 ///
177 unittest
178 {
179     import jaster.stream.memory : MemoryStreamGC;
180 
181     auto stream = new MemoryStreamGC();
182     stream.write([0, 1, 2, 3, 4, 5]);
183     stream.position = 0;
184 
185     auto range = stream.byChunkGC(4);
186     assert(!range.empty);
187     assert(range.front == [0, 1, 2, 3]);
188     
189     range.popFront();
190     assert(!range.empty);
191     assert(range.front == [4, 5]);
192 
193     range.popFront();
194     assert(range.empty);
195 }