1 module jaster.stream.binary;
2 
3 private
4 {
5     import std.exception : enforce;
6     import std.traits    : isNumeric, isSomeString, isDynamicArray;
7     import std.range     : ElementType;
8     import std.bitmanip  : bigEndianToNative, nativeToBigEndian, littleEndianToNative, nativeToLittleEndian;
9     import std.utf       : validate;
10     import std.format    : format;
11     import std.typecons  : Nullable;
12     import jaster.stream.core;
13 }
14 
15 public import std.system : Endian, systemEndian = endian;
16 
17 /++
18  + A class used to easily write and read bytes to and from a `Stream`.
19  +
20  + This is useful for encoding data into a binary format.
21  +
22  + Notes:
23  +  Functions are marked as either [Easy] or [Advanced].
24  +
25  +  [Easy] functions are designed to be easy to use, and should be fine
26  +  for general cases.
27  +
28  +  [Advanced] functions are a bit less "automatic" or perform some other function,
29  +  and are best used when the binary data needs to be a bit more customised.
30  +
31  + Throws:
32  +  All read functions except `readBytes` will throw a `StreamException` if they suddenly reach the end of the stream when reading.
33  +
34  +  All write functions will throw a `StreamException` if they couldn't write out all of their bytes.
35  + ++/
36 final class BinaryIO
37 {
38     private
39     {
40         Stream _stream;
41         Endian _endian;
42     }
43 
44     /++
45      + Assertions:
46      +  `stream` must not be `null`.
47      + ++/    
48     this(Stream stream, Endian endian = systemEndian)
49     {
50         assert(stream !is null);
51         this._stream        = stream;
52         this._endian = endian;
53     }
54 
55     // ###########
56     // # READING #
57     // ###########
58     public
59     {
60         /++
61          + [Advanced] Reads a certain amount of bytes.
62          +
63          + Notes:
64          +  This is a slice into the underlying data instead of a copy.
65          + ++/
66         ubyte[] readBytes(size_t amount)
67         {
68             if(amount == 0)
69                 return null;
70 
71             return this._stream.read(amount);
72         }
73 
74         /++
75          + [Advanced] Reads in a length of something in a compact format.
76          + ++/
77         size_t readLengthBytes()
78         {
79             ubyte[4] data;
80             data[0]   = this.read!ubyte();
81             auto info = (this.endian == Endian.bigEndian)
82                         ? data[0] & 0b1100_0000
83                         : (data[0] & 0b0000_0011) << 6;
84 
85             T doConvert(T)(ubyte[T.sizeof] data)
86             {
87                 auto v = (this.endian == Endian.bigEndian)
88                          ? bigEndianToNative!T(data)
89                          : littleEndianToNative!T(data);
90 
91                 return (this.endian == Endian.bigEndian) 
92                        ? v & ~(0b11 << ((T.sizeof * 8) - 2)) 
93                        : v >> 2;
94             }
95 
96             if(info == 0)
97                 return doConvert!ubyte(data[0..1]);
98             else if(info == 0b0100_0000)
99             {
100                 data[1] = this.read!ubyte();
101                 return doConvert!ushort(data[0..2]);
102             }
103             else if(info == 0b1000_0000)
104             {
105                 data[1..$] = this.readBytes(3);
106                 return doConvert!uint(data[0..4]);
107             }
108             else
109                 throw new Exception("Length size info 0b1100_0000 is not used right now.");
110         }
111 
112         /++
113          + [Easy] Reads in a single numeric value.
114          + ++/
115         T read(T)()
116         if(isNumeric!T)
117         {
118             auto bytes = this.readBytes(T.sizeof);
119             enforce!StreamException(bytes.length == T.sizeof, format("Expected %s bytes, but only got %s.", T.sizeof, bytes.length));
120             return (this.endian == Endian.bigEndian) 
121                    ? bigEndianToNative!T(cast(ubyte[T.sizeof])bytes[0..T.sizeof])
122                    : littleEndianToNative!T(cast(ubyte[T.sizeof])bytes[0..T.sizeof]);
123         }
124 
125         /++
126          + [Easy] Reads in an array of numeric values.
127          + ++/
128         T read(T)()
129         if(isNumeric!(ElementType!T) && isDynamicArray!T)
130         {
131             auto length = this.readLengthBytes();
132             T arr;
133             foreach(i; 0..length)
134                 arr ~= this.read!(ElementType!T)();
135 
136             return arr;
137         }
138 
139         /++
140          + [Easy] Reads in a string.
141          +
142          + Notes:
143          +  If the character type isn't immutable, then a slice to the underlying data is returned instead of
144          +  a copy.
145          +
146          +  If it is immutable, then a `.idup` of the slice is returned.
147          + ++/
148         T read(T)()
149         if(isSomeString!T)
150         {
151             auto length = this.readLengthBytes();
152             auto slice  = this.readBytes(length);
153             T    data;
154 
155             static if(is(ElementType!T == immutable))
156                 data = cast(T)slice.idup;
157             else
158                 data = cast(T)slice;
159 
160             validate(data);
161             return data;
162         }
163     }
164 
165     // ###########
166     // # WRITING #
167     // ###########
168     public
169     {
170         /++
171          + [Advanced] Writes out a series of bytes into the stream.
172          +
173          + Notes:
174          +  $(B All) other write functions are based off of this function.
175          +
176          +  This function will grow the stream's size if needed.
177          +
178          +  Use this function if the [Easy] functions don't fit your use case.
179          + ++/
180         void writeBytes(scope const ubyte[] data)
181         {
182             auto amount = this._stream.write(data).length;
183             enforce!StreamException(amount == data.length, format("Expected to write %s bytes, but could only write %s bytes.", data.length, amount));
184         }
185 
186         /++
187          + [Advanced] Writes out a length in a compact format.
188          +
189          + Details:
190          +  This function aims to minimize the amount of bytes needed to write out the length of an array.
191          +
192          +  If the length is <= to 63, then a single byte is used.
193          +
194          +  If the length is <= to 16,383, then two bytes are used.
195          +
196          +  If the length is <= to 1,073,741,823, then four bytes are used.
197          +
198          +  In some cases it may be better to set a strict byte limit for an array, so this function may not be useful.
199          +
200          + Notes:
201          +  Use this function if you need to write out the length of an array (outside of the [Easy] functions).
202          +
203          +  The last two bits are reserved for size info, so the max value of `length` is (2^30)-1, or 1,073,741,823
204          +
205          +  In big endian, the last two bits are used, since they will appear first in the data output.
206          +  In little endian, the number is shifted to the left by two, so the first two bits can be used, again,
207          +  because the first two bits will be in the first byte of the output.
208          +
209          + Throws:
210          +  `Exception` if `length` is greater than 0x3FFFFFFF.
211          + ++/
212         void writeLengthBytes(size_t length)
213         {
214             // Last two bits are reserved for size info.
215             // 00 = Length is one byte.
216             // 01 = Length is two bytes.
217             // 10 = Length is four bytes.
218             enforce(length <= 0b00111111_11111111_11111111_11111111, "Length is too much");
219             auto toWrite = (this.endian == Endian.bigEndian) ? length : length << 2;
220 
221             if(length <= 0b00111111) // Single byte
222                 this.write!ubyte(cast(ubyte)toWrite);
223             else if(length <= 0b00111111_11111111) // Two bytes
224             {
225                 toWrite |= (this.endian == Endian.bigEndian)
226                            ? 0b01000000_00000000
227                            : 0b00000000_00000001;
228                 this.write!ushort(cast(ushort)toWrite);
229             }
230             else // Four bytes
231             {
232                 toWrite |= (this.endian == Endian.bigEndian)
233                            ? 0b10000000_00000000_00000000_00000000
234                            : 0b00000000_00000000_00000000_00000010;
235                 this.write!uint(cast(uint)toWrite);
236             }
237         }
238 
239         /++
240          + [Easy] Writes a single numeric value.
241          + ++/
242         void write(T)(T value)
243         if(isNumeric!T)
244         {
245             auto bytes = (this.endian == Endian.bigEndian) ? value.nativeToBigEndian
246                                                            : value.nativeToLittleEndian;
247             this.writeBytes(bytes[]);
248         }
249 
250         /++
251          + [Easy] Writes an array of numeric values.
252          + ++/
253         void write(T)(T[] value)
254         if(isNumeric!T)
255         {
256             this.writeLengthBytes(value.length);
257             foreach(val; value)
258                 this.write!T(val);
259         }
260 
261         /++
262          + [Easy] Writes a string.
263          + ++/
264         void write(T)(T[] value)
265         if(is(T : const(char)) && !is(Unqual!T == ubyte))
266         {
267             auto bytes = cast(ubyte[])value;
268             this.writeLengthBytes(bytes.length);
269             this.writeBytes(bytes);
270         }
271     }
272 
273     // #########
274     // # OTHER #
275     // #########
276     public
277     {
278         ///
279         @property @safe @nogc
280         inout(S) stream(S : Stream = Stream)() nothrow inout
281         {
282             return cast(inout(S))this._stream;
283         }
284 
285         /// Sets the endianess of the numbers written/read by the high level functions.
286         @property @safe @nogc
287         inout(Endian) endian() nothrow inout
288         {
289             return this._endian;
290         }
291 
292         ///
293         @property @safe @nogc
294         void endian(Endian e) nothrow
295         {
296             this._endian = e;
297         }
298     }
299 }
300 ///
301 unittest
302 {
303     import std.algorithm : reverse;
304     import jaster.stream.memory;
305 
306     void doTest(Endian endian)
307     {
308         auto stream = new BinaryIO(new MemoryStreamGC());
309         stream.endian = endian;
310 
311         // # Test numeric writes #
312         stream.write!short(cast(short)0xFEED);
313         stream.write!int(0xDEADBEEF);
314         assert(stream.stream.length == 6);
315 
316         auto expected1 = (endian == Endian.bigEndian)
317                          ? [0xFE, 0xED, 0xDE, 0xAD, 0xBE, 0xEF]
318                          : [0xED, 0xFE, 0xEF, 0xBE, 0xAD, 0xDE];
319 
320         stream.stream.position = 0;
321         assert(stream.readBytes(6) == expected1);
322 
323         stream.stream.position = 0;
324         assert(stream.read!short == cast(short)0xFEED);
325         assert(stream.read!int   == 0xDEADBEEF);
326 
327         // # Test length byte writes #
328         stream = new BinaryIO(new MemoryStreamGC());
329         stream.endian = endian;
330 
331         stream.writeLengthBytes(60);       // One byte
332         stream.writeLengthBytes(16_000);   // Two bytes
333         stream.writeLengthBytes(17_000);   // Four bytes
334 
335         auto expected2 = 
336         (endian == Endian.bigEndian)
337         ? [0b0011_1100,
338            0b0111_1110, 0b1000_0000,
339            0b1000_0000, 0b0000_0000, 0b0100_0010, 0b0110_1000]
340         : [0b1111_0000,
341            0b0000_0001, 0b1111_1010,
342            0b1010_0010, 0b0000_1001, 0b0000_0001, 0b0000_0000];
343 
344         stream.stream.position = 0;
345         assert(stream.readBytes(7) == expected2);
346                                     
347         stream.stream.position = 0;
348         assert(stream.readLengthBytes() == 60);
349         assert(stream.readLengthBytes() == 16_000);
350         assert(stream.readLengthBytes() == 17_000);
351 
352         // # Test array writes #
353         stream = new BinaryIO(new MemoryStreamGC());
354         stream.endian = endian;
355 
356         stream.write!ushort([0xAABB, 0xCCDD, 0xEEFF]);
357 
358         auto expected3 = 
359         (endian == Endian.bigEndian)
360         ? [0x03, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]
361         : [0x0C, 0xBB, 0xAA, 0xDD, 0xCC, 0xFF, 0xEE];
362 
363         stream.stream.position = 0;
364         assert(stream.readBytes(7) == expected3);
365 
366         stream.stream.position = 0;
367         assert(stream.read!(ushort[]) == [0xAABB, 0xCCDD, 0xEEFF]);
368 
369         // # Test string writes #
370         stream = new BinaryIO(new MemoryStreamGC());
371         stream.endian = endian;
372 
373         stream.write("Gurl");
374 
375         auto expected4 = (endian == Endian.bigEndian) ? 0x04 : 0x04 << 2;
376         stream.stream.position = 0;
377         assert(stream.readBytes(stream.stream.length) == [expected4, 'G', 'u', 'r', 'l']);
378 
379         stream.stream.position = 0;
380         assert(stream.read!string() == "Gurl");
381     }
382 
383     doTest(Endian.bigEndian);
384     doTest(Endian.littleEndian);
385 }