module structs; private { import core.exception; import std.array; import std.bitmanip; import std.conv; import std.exception; import std.format; import std.meta; import std.range; import std.string; import std.system; import std.typecons; import structs.endian; /++ + Supported format types +/ enum FormatType { int8, /// Signed integer 8 bit int16, /// Signed integer 16 bit int32, /// Signed integer 32 bit int64, /// Signed integer 64 bit uint8, /// Unsigned integer 8 bit uint16, /// Unsigned integer 16 bit uint32, /// Unsigned integer 32 bit uint64, /// Unsigned integer 64 bit str /// C-String } /++ + A element of a package +/ struct Element { Endian endian; /// The endian to use bool isArray; /// If it's an array size_t arraySize; /// Array size FormatType formatType; /// The formated type pure nothrow @nogc size_t baseSize() const { final switch (this.formatType) { case FormatType.str: case FormatType.int8: case FormatType.uint8: return 1; case FormatType.int16: case FormatType.uint16: return 2; case FormatType.int32: case FormatType.uint32: return 4; case FormatType.int64: case FormatType.uint64: return 8; } } /++ + Calculate the size of the element + Returns: Size in bytes +/ pure nothrow @nogc size_t packSize() const { // Base format type size size_t baseSize = this.baseSize(); // Add array size return baseSize * this.arraySize; } pure Element genBase() const { Element result = {}; result.endian = this.endian; result.isArray = false; result.arraySize = 1; result.formatType = this.formatType; return result; } static pure Element genFromString(string data) in (data.length > 0, "Format can't be empty.") { // Pre checks size_t currentPos = 0; // Read endian bool endianExplicit = false; Endian endianFound = std.system.endian; switch (data[0]) { case '=': endianFound = std.system.endian; currentPos++; endianExplicit = true; break; case '<': endianFound = Endian.littleEndian; currentPos++; endianExplicit = true; break; case '!': case '>': endianFound = Endian.bigEndian; currentPos++; endianExplicit = true; break; default: } // Get size bool setSize = false; size_t size = 0; while ((currentPos < data.length) && ('0' <= data[currentPos]) && (data[currentPos] <= '9')) { setSize = true; size = (size * 10) + (data[currentPos] - '0'); currentPos++; } if (!setSize) { size = 1; } // Get format assert(currentPos + 1 == data.length, "Format '" ~ data[currentPos .. $] ~ "' isn't a valid format type."); FormatType formatTypeFound; switch (data[currentPos]) { case 'b': formatTypeFound = FormatType.int8; currentPos++; break; case 'B': formatTypeFound = FormatType.uint8; currentPos++; break; case 'h': formatTypeFound = FormatType.int16; currentPos++; break; case 'H': formatTypeFound = FormatType.uint16; currentPos++; break; case 'i': formatTypeFound = FormatType.int32; currentPos++; break; case 'I': formatTypeFound = FormatType.uint32; currentPos++; break; case 'q': formatTypeFound = FormatType.int64; currentPos++; break; case 'Q': formatTypeFound = FormatType.uint64; currentPos++; break; case 's': assert(setSize, "Size have to be set for the string."); formatTypeFound = FormatType.str; currentPos++; break; default: assert(false, "Unknown format string: '" ~ data[currentPos .. $] ~ "'"); } // Check endians assert((formatTypeFound != FormatType.str) || !endianExplicit, "Endian isn't allowed for strings."); // Is string ==> not endian set assert(((formatTypeFound != FormatType.int8) && (formatTypeFound != FormatType.uint8) && (formatTypeFound != FormatType.str)) || !endianExplicit, "Endian isn't allowed for (unsigned) bytes or strings."); // Is byte ==> not endian set assert((formatTypeFound == FormatType.str) || (formatTypeFound == FormatType.int8) || (formatTypeFound == FormatType.uint8) || endianExplicit, "Endian is required for non string or (unsigned) char."); // Is not string, int8 or uint8 ==> endian set // Return new struct assert(currentPos == data.length); Element result = {endian: endianFound, isArray: setSize, arraySize: size, formatType: formatTypeFound}; return result; } } /++ + Generate type of an element +/ template GetType(Element ELEMENT) { static if (ELEMENT.formatType == FormatType.str) { alias GetType = string; } else { // Get type static if (ELEMENT.formatType == FormatType.int8) { alias TMP = byte; } else static if (ELEMENT.formatType == FormatType.int16) { alias TMP = short; } else static if (ELEMENT.formatType == FormatType.int32) { alias TMP = int; } else static if (ELEMENT.formatType == FormatType.int64) { alias TMP = long; } else static if (ELEMENT.formatType == FormatType.uint8) { alias TMP = ubyte; } else static if (ELEMENT.formatType == FormatType.uint16) { alias TMP = ushort; } else static if (ELEMENT.formatType == FormatType.uint32) { alias TMP = uint; } else static if (ELEMENT.formatType == FormatType.uint64) { alias TMP = ulong; } else { static assert(false); } // Is array static if (ELEMENT.isArray) { alias GetType = TMP[ELEMENT.arraySize]; } else { alias GetType = TMP; } } } /++ + Removes the whitespaces of the string. + Params: + source = The source string to format + Returns: String without whitespaces +/ pure string removeWhitespaces(string source) { return source.replace(" ", ""); } pure Element[] parseString(string source) { // Remove whitespaces source = removeWhitespaces(source); // Split after char Element[] elements = []; size_t pos = 0; size_t last = 0; while (pos < source.length) { if ((source[pos] >= 'a' && source[pos] <= 'z') || (source[pos] >= 'A' && source[pos] <= 'Z')) { elements ~= [Element.genFromString(source[last .. pos + 1])]; last = pos + 1; } pos++; } assert(last == pos, "Format doesn't end correctly."); return elements; } // Test remove whitespaces. unittest { assert(removeWhitespaces(" a b c ") == "abc"); } // Test endian and length unittest { assertThrown!AssertError(Element.genFromString("i")); { const auto tmp = Element.genFromString("=i"); assert(tmp.endian == endian); assert(tmp.isArray == false); assert(tmp.arraySize == 1); assert(tmp.formatType == FormatType.int32); } { const auto tmp = Element.genFromString(">i"); assert(tmp.endian == Endian.bigEndian); assert(tmp.isArray == false); assert(tmp.arraySize == 1); assert(tmp.formatType == FormatType.int32); } { const auto tmp = Element.genFromString("!i"); assert(tmp.endian == Endian.bigEndian); assert(tmp.isArray == false); assert(tmp.arraySize == 1); assert(tmp.formatType == FormatType.int32); } { const auto tmp = Element.genFromString("16i=16i!16i"); foreach (i; tmp) { assert(i.isArray == true); assert(i.arraySize == 16); assert(i.formatType == FormatType.int32); } } assertThrown!AssertError(parseString("i16")); assertThrown!AssertError(parseString("i!")); assertThrown!AssertError(parseString("16!i")); } // Test helper functions unittest { { Element tmp = {}; tmp.arraySize = 10; tmp.endian = Endian.littleEndian; tmp.formatType = FormatType.int8; tmp.isArray = true; const Element test = tmp.genBase(); assert(test.arraySize == 1); assert(test.endian == Endian.littleEndian); assert(test.formatType == FormatType.int8); assert(test.isArray == false); assert(tmp.arraySize == 10); assert(tmp.endian == Endian.littleEndian); assert(tmp.formatType == FormatType.int8); assert(tmp.isArray == true); } } } public { /++ + A format error when formating wasn't possible. +/ class FormatError : Exception { /++ + Constructor of an format error. + Params: + msg = Error message. + nextInChain = Next error in the chain. +/ this(string msg, Throwable nextInChain = null) pure nothrow @nogc @safe { super(msg, nextInChain); } } /++ + Base format type. + + A format string item contains of free options: "[Endian][Array size][Type]" + + Endian can be "<" for little endian; ">", "!" for big endian; and "=" for native (highly not recommanded). + + Array size is optional except for c-strings. + + The type can be: + - "b"/"B" for a signed 8 bit integer. Setting an endian isn't allowed. + - "h"/"H" for a signed 16 bit integer. + - "i"/"I" for an 32 bit integer. + - "q"/"Q" for a signed 64 bit integer. + - "s" for a c-string. The array size descripes the maximal length of the c-string including the terminating + zero byte. Should the string be smaller then the given length will be expected (and gerated) additnal zero + bytes after the end of the string. Should the string be to long a exception will be thrown. Should a c-string + doesn't end with a valid zero byte during unpacking an exception will be thrown. Should during unpacking of + a c-string after the first zero byte follow additinal non zero-bytes inside the reservated space will be the + additional data be ignored. During unpacking a c-string will be the tailing zero byte removed and during + be added. Setting an endian isn't allowed. + + Macros: + CONFIG = The config string of the byte format. +/ struct BaseFormat(string CONFIG) { private { static immutable(Element[]) elements = parseString(CONFIG); static immutable(size_t) elementsSize = { size_t size = 0; static foreach (i; elements) { size += i.packSize(); } return size; }(); static pure void unpackEntry(Element ELEMENT)(ref GetType!ELEMENT target, immutable(ubyte)[] source) in (source.length == ELEMENT.packSize())do { static if (ELEMENT.formatType == FormatType.str) { // Get length size_t len = 0; foreach (i; 0 .. source.length) { if (source[i] == 0) { break; } len++; } if (len >= ELEMENT.arraySize) { throw new FormatError(format!"C-String of size %d had no terminating zero byte."(ELEMENT .arraySize)); } // Output string target = (cast(immutable(char[])) source[0 .. len]); return; } else static if (ELEMENT.isArray) { static if (ELEMENT.endian == Endian.littleEndian) { static foreach (i; 0 .. ELEMENT.arraySize) { target[i] = loadLittleEndian!(GetType!(ELEMENT.genBase()))( source[(i * ELEMENT.baseSize()) .. ((i + 1) * ELEMENT.baseSize())]); } return; } else static if (ELEMENT.endian == Endian.bigEndian) { static foreach (i; 0 .. ELEMENT.arraySize) { target[i] = loadBigEndian!(GetType!(ELEMENT.genBase()))( source[(i * ELEMENT.baseSize()) .. ((i + 1) * ELEMENT.baseSize())]); } return; } else { static assert(false); // Should never happen } } else { static if (ELEMENT.endian == Endian.littleEndian) { target = loadLittleEndian!(GetType!ELEMENT)(source); return; } else static if (ELEMENT.endian == Endian.bigEndian) { target = loadBigEndian!(GetType!ELEMENT)(source); return; } else { static assert(false); // Should never happen } } } mixin({ string result = "alias ARGS = staticMap!(GetType"; foreach (i; 0 .. elements.length) { result ~= ", elements[" ~ to!string(i) ~ "]"; } return result ~ ");"; }()); } public { alias TUPLE = Tuple!(ARGS); /++ + Calculate the size of the format. + Returns: Size of the format +/ static pure nothrow @nogc size_t size() { return elementsSize; } /++ + +/ static pure TUPLE unpack(immutable(ubyte)[] source) { // Check input if (source.length != size()) { throw new FormatError(format!"Source has size %d but %d is required."(source.length, size())); } // Generate output auto result = TUPLE(); size_t pos = 0; static foreach (i; 0 .. elements.length) { unpackEntry!(elements[i])(result[i], source[pos .. pos + elements[i].packSize()]); pos += elements[i].packSize(); } return result; } } } /++ + Is a integer of type size_t with the length of base format string. + For a detailed documentation of a format string look at [BaseFormat]. +/ template baseSize(string FORMAT) { static const size_t baseSize = BaseFormat!FORMAT.size(); } /++ + Unpacks a base format string. + For a detailed documentation of a format string look at [BaseFormat]. +/ pure BaseFormat!FORMAT.TUPLE baseUnpack(string FORMAT)(immutable(ubyte)[] source) { return BaseFormat!FORMAT.unpack(source); } } private { // Test size unittest { size_t testSize(string FORMAT)() { size_t result = BaseFormat!FORMAT.size(); assert(baseSize!FORMAT == result); { auto tmp = parseString(FORMAT); size_t calced = 0; foreach (i; tmp) { calced += i.packSize(); } assert(result == calced); } return result; } assert(testSize!"10s"() == 10); assert(testSize!"b"() == 1); assert(testSize!"4b"() == 4); assert(testSize!"B"() == 1); assert(testSize!"4B"() == 4); assert(testSize!">h"() == 2); assert(testSize!">4h"() == 8); assert(testSize!">H"() == 2); assert(testSize!">4H"() == 8); assert(testSize!">i"() == 4); assert(testSize!">4i"() == 16); assert(testSize!">I"() == 4); assert(testSize!">4I"() == 16); assert(testSize!">q"() == 8); assert(testSize!">4q"() == 32); assert(testSize!">Q"() == 8); assert(testSize!">4Q"() == 32); } // Test pack and unpack unittest { template testPacking(string FORMAT) { void testPacking(TUPLE)(TUPLE data, immutable(ubyte[]) packed) { assert(BaseFormat!FORMAT.unpack(packed) == data); assert(baseUnpack!FORMAT(packed) == data); } } // Strings testPacking!"3s"(tuple(""), [0, 0, 0]); testPacking!"3s"(tuple("a"), ['a', 0, 0]); testPacking!"3s"(tuple("ab"), ['a', 'b', 0]); assertThrown!FormatError(BaseFormat!"3s".unpack(['a', 'b', 'c'])); // Bytes testPacking!"b"(tuple(1), [1]); testPacking!"b"(tuple(-1), [255]); testPacking!"B"(tuple(255), [255]); // Shorts testPacking!"h"(tuple(1), [0, 1]); testPacking!">h"(tuple(-1), [255, 255]); testPacking!"H"(tuple(0xFFFE), [0xFF, 0xFE]); // Integers testPacking!"i"(tuple(1), [0, 0, 0, 1]); testPacking!">i"(tuple(-1), [255, 255, 255, 255]); testPacking!"I"(tuple(0xFFFFFFFE), [0xFF, 0xFF, 0xFF, 0xFE]); // Longs testPacking!"q"(tuple(1), [0, 0, 0, 0, 0, 0, 0, 1]); testPacking!">q"(tuple(-1), [255, 255, 255, 255, 255, 255, 255, 255]); testPacking!"Q"(tuple(0xFFFFFFFFFFFFFFFE), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE]); // Array testPacking!"<1i"(tuple([1]), [1, 0, 0, 0]); testPacking!">1i"(tuple([1]), [0, 0, 0, 1]); testPacking!"<4i"(tuple([1, 2, 3, 4]), [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0]); testPacking!">4i"(tuple([1, 2, 3, 4]), [0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4]); // Multiple types after each other testPacking!"<4i>Q"(tuple([1, 2, 3, 4], 0xFFFFFFFFFFFFFFFE), [ 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE ]); testPacking!"4i"(tuple(0xFFFFFFFFFFFFFFFE, [1, 2, 3, 4]), [ 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4 ]); // Except size error assertThrown!FormatError(testPacking!">Q"(tuple(0xFFFFFFFFFFFFFFFE), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])); } }