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 }