├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO.txt ├── dub.json ├── dub.selections.json ├── reggaefile.d ├── src └── cerealed │ ├── attrs.d │ ├── cereal.d │ ├── cerealiser.d │ ├── cerealizer.d │ ├── decerealiser.d │ ├── decerealizer.d │ ├── package.d │ ├── range.d │ ├── scopebuffer.d │ ├── traits.d │ └── utils.d └── tests ├── bugs.d ├── cerealiser_impl.d ├── classes.d ├── compile_time.d ├── decode.d ├── encode.d ├── encode_decode.d ├── enums.d ├── example.d ├── multidimensional_array.d ├── nested.d ├── pointers.d ├── property.d ├── protocol_unit.d ├── range.d ├── reset.d ├── static_array.d ├── structs.d └── utils.d /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Dub Test 7 | strategy: 8 | matrix: 9 | os: 10 | - ubuntu-20.04 11 | - windows-2019 12 | - macos-11 13 | dc: 14 | - dmd-2.103.1 15 | - ldc-1.32.2 16 | 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Install D compiler 22 | uses: dlang-community/setup-dlang@v1.3.0 23 | with: 24 | compiler: ${{ matrix.dc }} 25 | 26 | - name: Run tests 27 | run: dub test -q --build=unittest-cov 28 | 29 | - name: Build binary 30 | run: dub build -q 31 | 32 | - uses: codecov/codecov-action@v3 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.a 3 | *.o 4 | .dub 5 | .reggae 6 | *.ninja 7 | .ninja* 8 | compile_commands.json 9 | ut 10 | utl 11 | *.lst 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - d: dmd-nightly 7 | - d: dmd-2.093.1 8 | - d: ldc-1.23.0 9 | allow_failures: 10 | - d: dmd-nightly 11 | 12 | install: 13 | - mkdir bin 14 | 15 | script: 16 | - dub test --build=unittest-cov --compiler=${DC} 17 | # - dub test --build=release 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Atila Neves 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of cerealed nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cerealed 2 | ============= 3 | [![Build Status](https://travis-ci.org/atilaneves/cerealed.png?branch=master)](https://travis-ci.org/atilaneves/cerealed) 4 | [![Coverage](https://codecov.io/gh/atilaneves/cerealed/branch/master/graph/badge.svg)](https://codecov.io/gh/atilaneves/cerealed) 5 | 6 | [My DConf 2014 talk mentioning Cerealed](https://www.youtube.com/watch?v=xpImt14KTdc). 7 | 8 | Binary serialisation library for D. Minimal to no boilerplate necessary. Example usage: 9 | 10 | ```d 11 | import cerealed; 12 | 13 | assert(cerealise(5) == [0, 0, 0, 5]); // returns ubyte[] 14 | cerealise!(a => assert(a == [0, 0, 0, 5]))(5); // faster than using the bytes directly 15 | 16 | assert(decerealise!int([0, 0, 0, 5]) == 5); 17 | 18 | struct Foo { int i; } 19 | const foo = Foo(5); 20 | // alternate spelling 21 | assert(foo.cerealize.decerealize!Foo == foo); 22 | ``` 23 | 24 | The example below shows off a few features. First and foremost, members are serialised 25 | automatically, but can be opted out via the `@NoCereal` attribute. Also importantly, 26 | members to be serialised in a certain number of bits (important for binary protocols) 27 | are signalled with the `@Bits` attribute with a compile-time integer specifying the 28 | number of bits to use. 29 | 30 | ```d 31 | struct MyStruct { 32 | ubyte mybyte1; 33 | @NoCereal uint nocereal1; //won't be serialised 34 | @Bits!4 ubyte nibble; 35 | @Bits!1 ubyte bit; 36 | @Bits!3 ubyte bits3; 37 | ubyte mybyte2; 38 | } 39 | 40 | assert(MyStruct(3, 123, 14, 1, 2, 42).cerealise == [ 3, 0xea /*1110 1 010*/, 42]); 41 | ``` 42 | 43 | What if custom serialisation is needed and the default, even with opt-outs, won't work? 44 | If an aggregate type defines a member function `void accept(C)(ref C cereal)` it will be used 45 | instead. To get the usual automatic serialisation from within the custom `accept`, 46 | the `grainAllMembers` member function of Cereal can be called, as shown in the 47 | example below. This function takes a ref argument so rvalues need not apply. 48 | 49 | The function to use on `Cereal` to marshall or unmarshall a particular value is `grain`. 50 | This is essentially what `Cerealiser.~=` and `Decerealiser.value` are calling behind 51 | the scenes (and therefore `cerealise` and `decerealise`). 52 | 53 | ```d 54 | struct CustomStruct { 55 | ubyte mybyte; 56 | ushort myshort; 57 | void accept(C)(auto ref C cereal) { 58 | //do NOT call cereal.grain(this), that would cause an infinite loop 59 | cereal.grainAllMembers(this); 60 | ubyte otherbyte = 4; //make it an lvalue 61 | cereal.grain(otherbyte); 62 | } 63 | } 64 | 65 | assert(CustomStruct(1, 2).cerealise == [ 1, 0, 2, 4]); 66 | 67 | //because of the custom serialisation, passing in just [1, 0, 2] would throw 68 | assert([1, 0, 2, 4].decerealise!CustomStruct == CustomStruct(1, 2)); 69 | ``` 70 | 71 | The other option when custom serialisation is needed that avoids boilerplate is to 72 | define a `void postBlit(C)(ref C cereal)` function instead of `accept`. The 73 | marshalling or unmarshalling is done as it would in the absence of customisation, 74 | and `postBlit` is called to fix things up. It is a compile-time error to 75 | define both `accept` and `postBlit`. Example below. 76 | 77 | ```d 78 | struct CustomStruct { 79 | ubyte mybyte; 80 | ushort myshort; 81 | @NoCereal ubyte otherByte; 82 | void postBlit(C)(auto ref C cereal) { 83 | //no need to handle mybyte and myshort, already done 84 | if(mybyte == 1) { 85 | cereal.grain(otherByte); 86 | } 87 | } 88 | } 89 | 90 | assert(CustomStruct(1, 2).cerealise == [ 1, 0, 2, 4]); 91 | assert(CustomStruct(3, 2).cerealise == [ 1, 0, 2]); 92 | ``` 93 | 94 | For more examples of how to serialise structs, check the [tests](tests) directory 95 | or real-world usage in my [MQTT broker](https://github.com/atilaneves/mqtt) 96 | also written in D. 97 | 98 | Arrays are by default serialised with a ushort denoting array length followed 99 | by the array contents. It happens often enough that networking protocols 100 | have explicit length parameters for the whole packet and that array lengths 101 | are implicitly determined from this. For this use case, the `@RestOfPacket` 102 | attribute tells `cerealed` to not add the length parameter. As the name implies, 103 | it will "eat" all bytes until there aren't any left. 104 | 105 | ```d 106 | private struct StringsStruct { 107 | ubyte mybyte; 108 | @RestOfPacket string[] strings; 109 | } 110 | 111 | //no length encoding for the array, but strings still get a length each 112 | const bytes = [ 5, 0, 3, 'f', 'o', 'o', 0, 6, 'f', 'o', 'o', 'b', 'a', 'r', 113 | 0, 6, 'o', 'h', 'w', 'e', 'l', 'l']; 114 | const strs = StringStruct(5, ["foo", "foobar", "ohwell"]); 115 | assert(strs.cerealise == bytes); 116 | assert(bytes.decerealise!StringsStruct == strs); 117 | ``` 118 | 119 | Derived classes can be serialised via a reference to the base class, but the 120 | child class must be registered first: 121 | 122 | ```d 123 | class BaseClass { int a; this(int a) { this.a = a; }} 124 | class ChildClass { int b; this(int b) { this.b = b; }} 125 | Cereal.registerChildClass!ChildClass; 126 | BaseClass obj = ChildClass(3, 7); 127 | assert(obj.cerealise == [0, 0, 0, 3, 0, 0, 0, 7]); 128 | ``` 129 | 130 | There is now support for InputRange and OutputRange objects. Examples can 131 | be found in the [tests directory](tests/range.d) 132 | 133 | Advanced Usage 134 | --------------- 135 | Frequently in networking programming, the packets themselves encode the length 136 | of elements to follow. This happens often enough that Cerealed has two UDAs 137 | to automate this kind of serialisation: `@ArrayLength` and `@LengthInBytes`. 138 | The former specifies how to get the length of an array (usually a variable) 139 | The latter specifies how many bytes the array takes. Examples: 140 | 141 | ```d 142 | struct Packet { 143 | ushort length; 144 | @ArrayLength("length") ushort[] array; 145 | } 146 | auto pkt = decerealise!Packet([ 147 | 0, 3, //length 148 | 0, 1, 0, 2, 0, 3]); //array of 3 ushorts 149 | assert(pkt.length == 3); 150 | assert(pkt.array == [1, 2, 3]); 151 | 152 | struct Packet { 153 | static struct Header { 154 | ubyte ub; 155 | ubyte totalLength; 156 | } 157 | enum headerSize = unalignedSizeof!Header; //2 bytes 158 | 159 | Header header; 160 | @LengthInBytes("totalLength - headerSize") ushort[] array; 161 | } 162 | auto pkt = decerealise!Packet([ 163 | 7, //ub1 164 | 6, //totalLength in bytes 165 | 0, 1, 0, 2]); //array of 2 ushorts 166 | assert(pkt.ub1 == 7); 167 | assert(pkt.totalLength == 6); 168 | assert(pkt.array == [1, 2]); 169 | ``` 170 | 171 | Related Projects 172 | ---------------- 173 | - [orange](https://github.com/jacob-carlborg/orange). 174 | - [msgpack-d](https://github.com/msgpack/msgpack-d). 175 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - Throw if Bits!N is initialised with a value that is too big for N 2 | - Negative tests for when exceptions should be thrown 3 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerealed", 3 | "description": "Binary serialisation library for D", 4 | "homepage": "https://github.com/atilaneves/cerealed", 5 | "authors": ["Atila Neves"], 6 | "license": "BSD 3-clause", 7 | "targetType": "library", 8 | "targetPath": "bin", 9 | 10 | "dependencies": { 11 | "concepts": "~>0.0.6" 12 | }, 13 | "configurations": [ 14 | {"name": "library"}, 15 | { 16 | "name": "unittest", 17 | "targetName": "ut", 18 | "sourcePaths": ["tests"], 19 | "dependencies": { 20 | "unit-threaded": "*", 21 | "unit-threaded:autorunner": "*" 22 | } 23 | }, 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "concepts": "0.0.9", 5 | "unit-threaded": "2.1.6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /reggaefile.d: -------------------------------------------------------------------------------- 1 | import reggae; 2 | 3 | alias ut = dubTestTarget!(CompilerFlags("-w -g -debug"), 4 | LinkerFlags()); 5 | alias utl = dubConfigurationTarget!(Configuration("ut"), 6 | CompilerFlags("-w -g -debug -unittest -version=unitThreadedLight")); 7 | mixin build!(ut, utl); 8 | -------------------------------------------------------------------------------- /src/cerealed/attrs.d: -------------------------------------------------------------------------------- 1 | module cerealed.attrs; 2 | 3 | 4 | import std.typetuple; 5 | 6 | 7 | template getNumBits(T) { 8 | static if(is(T:Bits!N, int N)) { 9 | enum getNumBits = N; 10 | } else { 11 | enum getNumBits = 0; 12 | } 13 | } 14 | 15 | 16 | enum isABitsStruct(alias T) = is(T) && is(T:Bits!N, int N); 17 | 18 | 19 | struct Bits(int N) if(N > 0 && N <= 32) { 20 | } 21 | 22 | /** 23 | Exclude this member from serialization 24 | */ 25 | enum NoCereal; 26 | 27 | 28 | /** 29 | Do not encode array length before the array. 30 | This consumes the remaining bytes when deserializing 31 | */ 32 | enum RawArray; 33 | alias RestOfPacket = RawArray; 34 | alias Rest = RawArray; 35 | 36 | 37 | /** 38 | Inform the library about which member variable contains 39 | the length for an array measured in number of elements, not bytes 40 | */ 41 | struct ArrayLength { 42 | string member; 43 | } 44 | 45 | 46 | enum isArrayLengthStruct(alias T) = is(typeof(T)) && is(typeof(T) == ArrayLength); 47 | 48 | unittest { 49 | auto l = ArrayLength(); 50 | static assert(isArrayLengthStruct!l); 51 | } 52 | 53 | /** 54 | Specifies the length of an array by the number of bytes, not elements 55 | */ 56 | struct LengthInBytes { 57 | string member; 58 | } 59 | 60 | 61 | enum isLengthInBytesStruct(alias T) = is(typeof(T)) && is(typeof(T) == LengthInBytes); 62 | 63 | unittest { 64 | auto l = LengthInBytes(); 65 | static assert(isLengthInBytesStruct!l); 66 | } 67 | 68 | 69 | struct LengthType(T) { 70 | alias Type = T; 71 | } 72 | enum isLengthType(alias T) = is(T) && is(T:LengthType!U, U); 73 | 74 | unittest { 75 | static assert(isLengthType!(LengthType!ushort)); 76 | } 77 | -------------------------------------------------------------------------------- /src/cerealed/cereal.d: -------------------------------------------------------------------------------- 1 | module cerealed.cereal; 2 | 3 | import cerealed.traits: isCereal, isCerealiser, isDecerealiser; 4 | import std.traits; // too many to bother listing 5 | import std.range: isInputRange, isOutputRange, isInfinite; 6 | 7 | class CerealException: Exception { 8 | this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure { 9 | super(msg, file, line, next); 10 | } 11 | } 12 | 13 | enum CerealType { WriteBytes, ReadBytes }; 14 | 15 | void grain(C, T)(auto ref C cereal, ref T val) if(isCereal!C && is(T == ubyte)) { 16 | cereal.grainUByte(val); 17 | } 18 | 19 | //catch all signed numbers and forward to reinterpret 20 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && !is(T == enum) && 21 | (isSigned!T || isBoolean!T || 22 | is(T == char) || isFloatingPoint!T)) { 23 | cereal.grainReinterpret(val); 24 | } 25 | 26 | // If the type is an enum, get the unqualified base type and cast it to that. 27 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == enum)) { 28 | import std.conv: text; 29 | 30 | alias BaseType = Unqual!(OriginalType!(T)); 31 | cereal.grain( cast(BaseType)val ); 32 | if(val < T.min || val > T.max) 33 | throw new Exception(text("Illegal value (", val, ") for type ", T.stringof)); 34 | } 35 | 36 | 37 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && is(T == wchar)) { 38 | cereal.grain(*cast(ushort*)&val); 39 | } 40 | 41 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && is(T == dchar)) { 42 | cereal.grain(*cast(uint*)&val); 43 | } 44 | 45 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == ushort)) { 46 | ubyte valh = (val >> 8); 47 | ubyte vall = val & 0xff; 48 | cereal.grainUByte(valh); 49 | cereal.grainUByte(vall); 50 | val = (valh << 8) + vall; 51 | } 52 | 53 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == uint)) { 54 | ubyte val0 = (val >> 24); 55 | ubyte val1 = cast(ubyte)(val >> 16); 56 | ubyte val2 = cast(ubyte)(val >> 8); 57 | ubyte val3 = val & 0xff; 58 | cereal.grainUByte(val0); 59 | cereal.grainUByte(val1); 60 | cereal.grainUByte(val2); 61 | cereal.grainUByte(val3); 62 | val = (val0 << 24) + (val1 << 16) + (val2 << 8) + val3; 63 | } 64 | 65 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == ulong)) { 66 | T newVal; 67 | for(int i = 0; i < T.sizeof; ++i) { 68 | immutable shiftBy = 64 - (i + 1) * T.sizeof; 69 | ubyte byteVal = (val >> shiftBy) & 0xff; 70 | cereal.grainUByte(byteVal); 71 | newVal |= (cast(T)byteVal << shiftBy); 72 | } 73 | val = newVal; 74 | } 75 | 76 | enum hasByteElement(T) = is(Unqual!(ElementType!T): ubyte) && T.sizeof == 1; 77 | 78 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCerealiser!C && 79 | isInputRange!T && !isInfinite!T && 80 | !is(T == string) && 81 | !isStaticArray!T && 82 | !isAssociativeArray!T) { 83 | grain!ushort(cereal, val); 84 | } 85 | 86 | void grain(U, C, T)(auto ref C cereal, ref T val) @trusted if(isCerealiser!C && 87 | isInputRange!T && !isInfinite!T && 88 | !is(T == string) && 89 | !isStaticArray!T && 90 | !isAssociativeArray!T) { 91 | import std.conv: text; 92 | import std.array: array; 93 | import std.range: hasSlicing; 94 | 95 | enum hasLength = is(typeof(() { auto l = val.length; })); 96 | static assert(hasLength, text("Only InputRanges with .length accepted, not the case for ", 97 | fullyQualifiedName!T)); 98 | U length = cast(U)val.length; 99 | assert(length == val.length, 100 | text(C.stringof, " overflow. Length: ", length, ". Val length: ", val.length, "\n", 101 | val.array)); 102 | cereal.grain(length); 103 | 104 | static if(hasSlicing!(Unqual!T) && hasByteElement!T) 105 | cereal.grainRaw(cast(ubyte[])val.array); 106 | else 107 | foreach(ref e; val) cereal.grain(e); 108 | } 109 | 110 | 111 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && isStaticArray!T) { 112 | static if(hasByteElement!T) 113 | cereal.grainRaw(cast(ubyte[])val); 114 | else 115 | foreach(ref e; val) cereal.grain(e); 116 | } 117 | 118 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isDecerealiser!C && 119 | !isStaticArray!T && 120 | isOutputRange!(T, ubyte)) { 121 | grain!ushort(cereal, val); 122 | } 123 | 124 | void grain(U, C, T)(auto ref C cereal, ref T val) @trusted if(isDecerealiser!C && 125 | !isStaticArray!T && 126 | isOutputRange!(T, ubyte)) { 127 | version(DigitalMars) 128 | U length; 129 | else 130 | U length = void; 131 | 132 | cereal.grain(length); 133 | 134 | static if(isArray!T) { 135 | decerealiseArrayImpl(cereal, val, length); 136 | } else { 137 | for(U i = 0; i < length; ++i) { 138 | ubyte b = void; 139 | cereal.grain(b); 140 | 141 | enum hasOpOpAssign = is(typeof(() { val ~= b; })); 142 | static if(hasOpOpAssign) { 143 | val ~= b; 144 | } else { 145 | val.put(b); 146 | } 147 | } 148 | } 149 | } 150 | 151 | private void decerealiseArrayImpl(C, T, U)(auto ref C cereal, ref T val, U length) @safe 152 | if(is(T == E[], E) && isDecerealiser!C) 153 | { 154 | 155 | import std.exception: enforce; 156 | import std.conv: text; 157 | import std.range: ElementType, isInputRange; 158 | import std.traits: isScalarType; 159 | 160 | ulong neededBytes(T)(ulong length) { 161 | alias E = ElementType!T; 162 | static if(isScalarType!E) 163 | return length * E.sizeof; 164 | else static if(isInputRange!E) 165 | return neededBytes!E(length); 166 | else 167 | return 0; 168 | } 169 | 170 | immutable needed = neededBytes!T(length); 171 | enforce(needed <= cereal.bytesLeft, 172 | text("Not enough bytes left to decerealise ", T.stringof, " of ", length, " elements\n", 173 | "Bytes left: ", cereal.bytesLeft, ", Needed: ", needed, ", bytes: ", cereal.bytes)); 174 | 175 | static if(hasByteElement!T) { 176 | val = cereal.grainRaw(length).dup; 177 | } else { 178 | if(val.length != length) val.length = cast(uint)length; 179 | assert(length == val.length, "overflow"); 180 | 181 | foreach(ref e; val) cereal.grain(e); 182 | } 183 | } 184 | 185 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isDecerealiser!C && 186 | !isOutputRange!(T, ubyte) && 187 | isDynamicArray!T && !is(T == string)) { 188 | grain!ushort(cereal, val); 189 | } 190 | 191 | void grain(U, C, T)(auto ref C cereal, ref T val) @trusted if(isDecerealiser!C && 192 | !isOutputRange!(T, ubyte) && 193 | isDynamicArray!T && !is(T == string)) { 194 | version(DigitalMars) 195 | U length; 196 | else 197 | U length = void; 198 | 199 | cereal.grain(length); 200 | decerealiseArrayImpl(cereal, val, length); 201 | } 202 | 203 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && is(T == string)) { 204 | grain!ushort(cereal, val); 205 | } 206 | 207 | void grain(U, C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && is(T == string)) { 208 | U length = cast(U)val.length; 209 | assert(length == val.length, "overflow"); 210 | cereal.grain(length); 211 | 212 | static if(isCerealiser!C) 213 | cereal.grainRaw(cast(ubyte[])val); 214 | else 215 | val = cast(string) cereal.grainRaw(length).idup; 216 | } 217 | 218 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && isAssociativeArray!T) { 219 | grain!ushort(cereal, val); 220 | } 221 | 222 | void grain(U, C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && isAssociativeArray!T) { 223 | U length = cast(U)val.length; 224 | assert(length == val.length, "overflow"); 225 | cereal.grain(length); 226 | const keys = val.keys; 227 | 228 | for(U i = 0; i < length; ++i) { 229 | KeyType!T k = keys.length ? keys[i] : KeyType!T.init; 230 | auto v = keys.length ? val[k] : ValueType!T.init; 231 | 232 | cereal.grain(k); 233 | cereal.grain(v); 234 | val[k] = v; 235 | } 236 | } 237 | 238 | void grain(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && isPointer!T) { 239 | import std.traits; 240 | alias ValueType = PointerTarget!T; 241 | static if(isDecerealiser!C) { 242 | if(val is null) val = new ValueType; 243 | } 244 | cereal.grain(*val); 245 | } 246 | 247 | private template canCall(C, T, string func) { 248 | enum canCall = is(typeof(() { auto cer = C(); auto val = T.init; mixin("val." ~ func ~ "(cer);"); })); 249 | static if(!canCall && __traits(hasMember, T, func)) { 250 | pragma(msg, "Warning: '" ~ func ~ 251 | "' function defined for ", T, ", but does not compile for Cereal ", C); 252 | } 253 | } 254 | 255 | void grain(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && isAggregateType!T && 256 | !isInputRange!T && !isOutputRange!(T, ubyte)) { 257 | enum canAccept = canCall!(C, T, "accept"); 258 | enum canPreBlit = canCall!(C, T, "preBlit"); 259 | enum canPostBlit = canCall!(C, T, "postBlit"); 260 | 261 | static if(canAccept) { //custom serialisation 262 | static assert(!canPostBlit && !canPreBlit, "Cannot define both accept and pre/postBlit"); 263 | val.accept(cereal); 264 | } else { //normal serialisation, go through each member and possibly serialise 265 | static if(canPreBlit) { 266 | val.preBlit(cereal); 267 | } 268 | 269 | cereal.grainAllMembers(val); 270 | static if(canPostBlit) { //semi-custom serialisation, do post blit 271 | val.postBlit(cereal); 272 | } 273 | } 274 | } 275 | 276 | void grainAllMembers(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == struct)) { 277 | cereal.grainAllMembersImpl!T(val); 278 | } 279 | 280 | 281 | void grainAllMembers(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C && is(T == class)) { 282 | 283 | import std.conv: text; 284 | 285 | static if(isCerealiser!C) { 286 | assert(val !is null, "null value cannot be serialised"); 287 | } 288 | 289 | enum hasDefaultConstructor = is(typeof(() { val = new T; })); 290 | static if(hasDefaultConstructor && isDecerealiser!C) { 291 | if(val is null) val = new T; 292 | } else { 293 | assert(val !is null, text("Cannot deserialise into null value. ", 294 | "Possible cause: no default constructor for ", 295 | fullyQualifiedName!T, ".")); 296 | } 297 | 298 | cereal.grainClass(val); 299 | } 300 | 301 | 302 | alias grainMemberWithAttr = grainAggregateMember; 303 | void grainAggregateMember(string member, C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C) { 304 | 305 | import cerealed.attrs: NoCereal; 306 | import std.meta: staticIndexOf; 307 | 308 | /**(De)serialises one member taking into account its attributes*/ 309 | enum noCerealIndex = staticIndexOf!(NoCereal, __traits(getAttributes, 310 | __traits(getMember, val, member))); 311 | //only serialise if the member doesn't have @NoCereal or @PostBlit 312 | static if(noCerealIndex == -1) { 313 | grainMember!member(cereal, val); 314 | } 315 | } 316 | 317 | void grainMember(string member, C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C) { 318 | 319 | import cerealed.attrs: 320 | isABitsStruct, isArrayLengthStruct, isLengthInBytesStruct, RawArray, isLengthType; 321 | import std.meta: staticIndexOf, Filter; 322 | 323 | alias bitsAttrs = Filter!(isABitsStruct, __traits(getAttributes, 324 | __traits(getMember, val, member))); 325 | static assert(bitsAttrs.length == 0 || bitsAttrs.length == 1, 326 | "Too many Bits!N attributes!"); 327 | 328 | alias arrayLengths = Filter!(isArrayLengthStruct, 329 | __traits(getAttributes, 330 | __traits(getMember, val, member))); 331 | static assert(arrayLengths.length == 0 || arrayLengths.length == 1, 332 | "Too many ArrayLength attributes"); 333 | 334 | alias lengthInBytes = Filter!(isLengthInBytesStruct, 335 | __traits(getAttributes, 336 | __traits(getMember, val, member))); 337 | static assert(lengthInBytes.length == 0 || lengthInBytes.length == 1, 338 | "Too many LengthInBytes attributes"); 339 | 340 | enum rawArrayIndex = staticIndexOf!(RawArray, __traits(getAttributes, 341 | __traits(getMember, val, member))); 342 | 343 | alias lengthTypes = Filter!(isLengthType, __traits(getAttributes, __traits(getMember, val, member))); 344 | static assert(lengthTypes.length == 0 || lengthTypes.length == 1, 345 | "Too many LengthType attributes"); 346 | 347 | static if(bitsAttrs.length == 1) { 348 | 349 | grainWithBitsAttr!(member, bitsAttrs[0])(cereal, val); 350 | 351 | } else static if(lengthTypes.length == 1) { 352 | 353 | grain!(lengthTypes[0].Type)(cereal, __traits(getMember, val, member)); 354 | 355 | } else static if(rawArrayIndex != -1) { 356 | 357 | cereal.grainRawArray(__traits(getMember, val, member)); 358 | 359 | } else static if(arrayLengths.length > 0) { 360 | 361 | grainWithArrayLengthAttr!(member, arrayLengths[0].member)(cereal, val); 362 | 363 | } else static if(lengthInBytes.length > 0) { 364 | 365 | grainWithLengthInBytesAttr!(member, lengthInBytes[0].member)(cereal, val); 366 | 367 | } else { 368 | 369 | cereal.grain(__traits(getMember, val, member)); 370 | 371 | } 372 | } 373 | 374 | private void grainWithBitsAttr(string member, alias bitsAttr, C, T)( 375 | auto ref C cereal, ref T val) @safe if(isCereal!C) { 376 | 377 | import cerealed.attrs: getNumBits; 378 | import std.conv: text; 379 | 380 | enum numBits = getNumBits!(bitsAttr); 381 | enum sizeInBits = __traits(getMember, val, member).sizeof * 8; 382 | static assert(numBits <= sizeInBits, 383 | text(fullyQualifiedName!T, ".", member, " is ", sizeInBits, 384 | " bits long, which is not enough to store @Bits!", numBits)); 385 | cereal.grainBitsT(__traits(getMember, val, member), numBits); 386 | } 387 | 388 | private void grainWithArrayLengthAttr(string member, string lengthMember, C, T) 389 | (auto ref C cereal, ref T val) @safe if(isCereal!C) { 390 | 391 | import std.conv: text; 392 | import std.range: ElementType; 393 | 394 | checkArrayAttrType!member(cereal, val); 395 | 396 | static if(isCerealiser!C) { 397 | cereal.grainRawArray(__traits(getMember, val, member)); 398 | } else { 399 | immutable length = lengthOfArray!(member, lengthMember)(cereal, val); 400 | alias E = ElementType!(typeof(__traits(getMember, val, member))); 401 | 402 | if(length * E.sizeof > cereal.bytesLeft) { 403 | throw new CerealException(text("@ArrayLength of ", length, " units of type ", 404 | E.stringof, 405 | " (", length * E.sizeof, " bytes) ", 406 | "larger than remaining byte array (", 407 | cereal.bytesLeft, " bytes)\n", 408 | cereal.bytes)); 409 | } 410 | 411 | mixin(q{__traits(getMember, val, member).length = length;}); 412 | 413 | foreach(ref e; __traits(getMember, val, member)) cereal.grain(e); 414 | } 415 | } 416 | 417 | void grainWithLengthInBytesAttr(string member, string lengthMember, C, T) 418 | (auto ref C cereal, ref T val) @safe if(isCereal!C) { 419 | 420 | import std.conv: text; 421 | import std.range: ElementType; 422 | 423 | checkArrayAttrType!member(cereal, val); 424 | 425 | static if(isCerealiser!C) { 426 | cereal.grainRawArray(__traits(getMember, val, member)); 427 | } else { 428 | immutable length = lengthOfArray!(member, lengthMember)(cereal, val); //error handling 429 | 430 | if(length > cereal.bytesLeft) { 431 | alias E = ElementType!(typeof(__traits(getMember, val, member))); 432 | throw new CerealException(text("@LengthInBytes of ", length, " bytes ", 433 | "larger than remaining byte array (", 434 | cereal.bytesLeft, " bytes)")); 435 | } 436 | 437 | __traits(getMember, val, member).length = 0; 438 | 439 | long bytesLeft = length; 440 | while(bytesLeft) { 441 | auto origCerealBytesLeft = cereal.bytesLeft; 442 | __traits(getMember, val, member).length++; 443 | cereal.grain(__traits(getMember, val, member)[$ - 1]); 444 | bytesLeft -= (origCerealBytesLeft - cereal.bytesLeft); 445 | } 446 | } 447 | } 448 | 449 | private void checkArrayAttrType(string member, C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C) { 450 | 451 | import std.conv: text; 452 | 453 | alias M = typeof(__traits(getMember, val, member)); 454 | static assert(is(M == E[], E), 455 | text("@ArrayLength and @LengthInBytes not valid for ", member, 456 | ": they can only be used on slices")); 457 | } 458 | 459 | 460 | private int lengthOfArray(string member, string lengthMember, C, T)(auto ref C cereal, ref T val) 461 | @safe if(isCereal!C) { 462 | 463 | import std.conv: text; 464 | 465 | int _tmpLen; 466 | mixin(q{with(val) _tmpLen = cast(int)(} ~ lengthMember ~ q{);}); 467 | 468 | if(_tmpLen < 0) 469 | throw new CerealException(text("@LengthInBytes resulted in negative length ", _tmpLen)); 470 | 471 | return _tmpLen; 472 | } 473 | 474 | void grainRawArray(C, T)(auto ref C cereal, ref T[] val) @trusted if(isCereal!C) { 475 | //can't use virtual functions due to template parameter 476 | static if(isDecerealiser!C) { 477 | val.length = 0; 478 | while(cereal.bytesLeft()) { 479 | val.length++; 480 | cereal.grain(val[$ - 1]); 481 | } 482 | } else { 483 | foreach(ref t; val) cereal.grain(t); 484 | } 485 | } 486 | 487 | 488 | /** 489 | * To be used when the length of the array is known at run-time based on the value 490 | * of a part of byte stream. 491 | */ 492 | void grainLengthedArray(C, T)(auto ref C cereal, ref T[] val, long length) { 493 | val.length = cast(typeof(val.length))length; 494 | foreach(ref t; val) cereal.grain(t); 495 | } 496 | 497 | 498 | package void grainClassImpl(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == class)) { 499 | //do base classes first or else the order is wrong 500 | cereal.grainBaseClasses(val); 501 | cereal.grainAllMembersImpl!T(val); 502 | } 503 | 504 | private void grainBitsT(C, T)(auto ref C cereal, ref T val, int bits) @safe if(isCereal!C) { 505 | uint realVal = val; 506 | cereal.grainBits(realVal, bits); 507 | val = cast(T)realVal; 508 | } 509 | 510 | private void grainReinterpret(C, T)(auto ref C cereal, ref T val) @trusted if(isCereal!C) { 511 | auto ptr = cast(CerealPtrType!T)(&val); 512 | cereal.grain(*ptr); 513 | } 514 | 515 | private void grainBaseClasses(C, T)(auto ref C cereal, ref T val) @safe if(isCereal!C && is(T == class)) { 516 | foreach(base; BaseTypeTuple!T) { 517 | cereal.grainAllMembersImpl!base(val); 518 | } 519 | } 520 | 521 | 522 | private void grainAllMembersImpl(ActualType, C, ValType) 523 | (auto ref C cereal, ref ValType val) @trusted if(isCereal!C) { 524 | foreach(member; __traits(derivedMembers, ActualType)) { 525 | //makes sure to only serialise members that make sense, i.e. data 526 | enum isMemberVariable = is(typeof(() { 527 | __traits(getMember, val, member) = __traits(getMember, val, member).init; 528 | })); 529 | static if(isMemberVariable) { 530 | cereal.grainAggregateMember!member(val); 531 | } 532 | } 533 | } 534 | 535 | private template CerealPtrType(T) { 536 | static if(is(T == bool) || is(T == char)) { 537 | alias CerealPtrType = ubyte*; 538 | } else static if(is(T == float)) { 539 | alias CerealPtrType = uint*; 540 | } else static if(is(T == double)) { 541 | alias CerealPtrType = ulong*; 542 | } else { 543 | alias CerealPtrType = Unsigned!T*; 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /src/cerealed/cerealiser.d: -------------------------------------------------------------------------------- 1 | module cerealed.cerealiser; 2 | 3 | 4 | import cerealed.cereal: grain; 5 | import cerealed.range: DynamicArrayRange, ScopeBufferRange, isCerealiserRange; 6 | import cerealed.traits: isCereal, isCerealiser; 7 | import concepts: models; 8 | import std.array: Appender; 9 | 10 | 11 | alias AppenderCerealiser = CerealiserImpl!(Appender!(ubyte[])); 12 | alias DynamicArrayCerealiser = CerealiserImpl!DynamicArrayRange; 13 | alias ScopeBufferCerealiser = CerealiserImpl!ScopeBufferRange; 14 | 15 | alias Cerealiser = AppenderCerealiser; //the default, easy option 16 | 17 | /** 18 | * Uses a ScopeBufferCerealiaser to write the bytes. The reason 19 | * it takes a function as a template parameter is to be able 20 | * to do something with the bytes. The bytes shouldn't be used 21 | * directly because once the function exits that is no longer 22 | * valid memory (it's been popped off the stack or freed). 23 | */ 24 | auto cerealise(alias F, ushort N = 32, T)(auto ref T val) @system { 25 | static assert(N % 2 == 0, "cerealise must be passed an even number of bytes"); 26 | ubyte[N] buf = void; 27 | auto sbufRange = ScopeBufferRange(buf); 28 | auto enc = ScopeBufferCerealiser(sbufRange); 29 | enc ~= val; 30 | static if(is(ReturnType!F == void)) { 31 | F(enc.bytes); 32 | } else { 33 | return F(enc.bytes); 34 | } 35 | } 36 | 37 | /** 38 | * Slower version of $(D cerealise) that returns a ubyte slice. 39 | * It's preferable to use the version with the lambda template alias 40 | */ 41 | ubyte[] cerealise(T)(auto ref T val) { 42 | auto enc = Cerealiser(); 43 | enc ~= val; 44 | return enc.bytes.dup; 45 | } 46 | 47 | alias cerealize = cerealise; 48 | 49 | @models!(Cerealiser, isCereal) 50 | @models!(Cerealiser, isCerealiser) 51 | struct CerealiserImpl(R) if(isCerealiserRange!R) { 52 | 53 | import cerealed.cereal: CerealType; 54 | import std.traits: isArray, isAssociativeArray, isDynamicArray, isAggregateType, Unqual; 55 | 56 | //interface 57 | enum type = CerealType.WriteBytes; 58 | 59 | void grainUByte(ref ubyte val) @trusted { 60 | _output.put(val); 61 | } 62 | 63 | void grainBits(ref uint value, int bits) @safe { 64 | writeBits(value, bits); 65 | } 66 | 67 | void grainClass(T)(T val) @trusted if(is(T == class)) { 68 | import cerealed.cereal: grainClassImpl; 69 | 70 | if(val.classinfo.name in _childCerealisers) { 71 | _childCerealisers[val.classinfo.name](this, val); 72 | } else { 73 | grainClassImpl(this, val); 74 | } 75 | } 76 | 77 | void grainRaw(ubyte[] val) @trusted { 78 | _output.put(val); 79 | } 80 | 81 | //specific: 82 | this(R r) { 83 | _output = r; 84 | } 85 | 86 | const(ubyte[]) bytes() const nothrow @property @safe { 87 | return _output.data; 88 | } 89 | 90 | ref CerealiserImpl opOpAssign(string op : "~", T)(T val) @safe { 91 | write(val); 92 | return this; 93 | } 94 | 95 | void write(T)(T val) @safe if(!isArray!T && !isAssociativeArray!T) { 96 | Unqual!T lval = val; 97 | grain(this, lval); 98 | } 99 | 100 | void write(T)(const ref T val) @safe if(!isDynamicArray!T && 101 | !isAssociativeArray!T && 102 | !isAggregateType!T) { 103 | T lval = val; 104 | grain(this, lval); 105 | } 106 | 107 | void write(T)(const(T)[] val) @trusted { 108 | auto lval = (cast(T[])val).dup; 109 | grain(this, lval); 110 | } 111 | 112 | void write(K, V)(const(V[K]) val) @trusted { 113 | auto lval = cast(V[K])val.dup; 114 | grain(this, lval); 115 | } 116 | 117 | void writeBits(in int value, in int bits) @safe { 118 | import std.conv: text; 119 | import std.exception: enforce; 120 | 121 | enforce(value < (1 << bits), text("value ", value, " too big for ", bits, " bits")); 122 | enum bitsInByte = 8; 123 | if(_bitIndex + bits >= bitsInByte) { //carries over to next byte 124 | const remainingBits = _bitIndex + bits - bitsInByte; 125 | const thisByteValue = (value >> remainingBits); 126 | _currentByte |= thisByteValue; 127 | grainUByte(_currentByte); 128 | _currentByte = 0; 129 | _bitIndex = 0; 130 | if(remainingBits > 0) { 131 | ubyte remainingValue = value & (0xff >> (bitsInByte - remainingBits)); 132 | writeBits(remainingValue, remainingBits); 133 | } 134 | return; 135 | } 136 | _currentByte |= (value << (bitsInByte - bits - _bitIndex)); 137 | _bitIndex += bits; 138 | } 139 | 140 | void reset() @safe { 141 | _output.clear(); 142 | } 143 | 144 | static void registerChildClass(T)() @safe { 145 | import cerealed.cereal: grainClassImpl; 146 | _childCerealisers[T.classinfo.name] = (ref Cerealiser cereal, Object val) { 147 | T child = cast(T)val; 148 | cereal.grainClassImpl(child); 149 | }; 150 | } 151 | 152 | private: 153 | 154 | R _output; 155 | ubyte _currentByte; 156 | int _bitIndex; 157 | alias ChildCerealiser = void function(ref CerealiserImpl cereal, Object val); 158 | static ChildCerealiser[string] _childCerealisers; 159 | 160 | // static assert(isCereal!CerealiserImpl); 161 | // static assert(isCerealiser!CerealiserImpl); 162 | } 163 | -------------------------------------------------------------------------------- /src/cerealed/cerealizer.d: -------------------------------------------------------------------------------- 1 | module cerealed.cerealizer; 2 | public import cerealed.cerealiser; 3 | alias Cerealizer = Cerealiser; 4 | alias cerealize = cerealise; 5 | -------------------------------------------------------------------------------- /src/cerealed/decerealiser.d: -------------------------------------------------------------------------------- 1 | module cerealed.decerealiser; 2 | 3 | import cerealed.cereal: grain; 4 | import cerealed.traits: isCereal, isDecerealiser; 5 | import concepts: models; 6 | 7 | auto decerealise(T)(in ubyte[] bytes) @trusted { 8 | return Decerealiser(bytes).value!T; 9 | } 10 | 11 | @models!(Decerealiser, isCereal) 12 | @models!(Decerealiser, isDecerealiser) 13 | struct Decerealiser { 14 | 15 | import cerealed.cereal: CerealType; 16 | import std.traits: isNumeric, isDynamicArray, isAssociativeArray; 17 | 18 | //interface: 19 | enum type = CerealType.ReadBytes; 20 | 21 | void grainUByte(ref ubyte val) @safe { 22 | val = _bytes[0]; 23 | _bytes = _bytes[1..$]; 24 | } 25 | 26 | void grainBits(ref uint value, int bits) @safe { 27 | value = readBits(bits); 28 | } 29 | 30 | void grainClass(T)(T val) @trusted if(is(T == class)) { 31 | import cerealed.cereal: grainClassImpl; 32 | grainClassImpl(this, val); 33 | } 34 | 35 | auto grainRaw(size_t length) @safe { 36 | auto res = _bytes[0..length]; 37 | _bytes = _bytes[length..$]; 38 | return res; 39 | } 40 | 41 | //specific: 42 | this(T)(in T[] bytes) @safe if(isNumeric!T) { 43 | setBytes(bytes); 44 | } 45 | 46 | const(ubyte[]) bytes() const nothrow @property @safe { 47 | return _bytes; 48 | } 49 | 50 | ulong bytesLeft() const @safe { return bytes.length; } 51 | 52 | @property T value(T)() if(!isDynamicArray!T && !isAssociativeArray!T && 53 | !is(T == class) && __traits(compiles, T())) { 54 | T val; 55 | grain(this, val); 56 | return val; 57 | } 58 | 59 | @property T value(T)() if(!isDynamicArray!T && !isAssociativeArray!T && 60 | !is(T == class) && !__traits(compiles, T())) { 61 | T val = void; 62 | grain(this, val); 63 | return val; 64 | } 65 | 66 | @property @trusted T value(T, A...)(A args) if(is(T == class)) { 67 | auto val = new T(args); 68 | grain(this, val); 69 | return val; 70 | } 71 | 72 | @property @safe T value(T)() if(isDynamicArray!T || isAssociativeArray!T) { 73 | return value!(T, ushort)(); 74 | } 75 | 76 | @property @safe T value(T, U)() if(isDynamicArray!T || isAssociativeArray!T) { 77 | T val; 78 | grain!U(this, val); 79 | return val; 80 | } 81 | 82 | 83 | void reset() @safe { 84 | /**resets the decerealiser to read from the beginning again*/ 85 | reset(_originalBytes); 86 | } 87 | 88 | void reset(T)(in T[] bytes) @safe if(isNumeric!T) { 89 | /**resets the decerealiser to use the new slice*/ 90 | _bitIndex = 0; 91 | _currentByte = 0; 92 | setBytes(bytes); 93 | } 94 | 95 | void read(T)(ref T val) @trusted { 96 | grain(this, val); 97 | } 98 | 99 | uint readBits(int bits) @safe { 100 | if(_bitIndex == 0) { 101 | _currentByte = this.value!ubyte; 102 | } 103 | 104 | return readBitsHelper(bits); 105 | } 106 | 107 | const(ubyte)[] originalBytes() @safe pure nothrow const { 108 | return _originalBytes; 109 | } 110 | 111 | private: 112 | 113 | const (ubyte)[] _originalBytes; 114 | const (ubyte)[] _bytes; 115 | ubyte _currentByte; 116 | int _bitIndex; 117 | 118 | uint readBitsHelper(int bits) @safe { 119 | enum bitsInByte = 8; 120 | if(_bitIndex + bits > bitsInByte) { //have to carry on to the next byte 121 | immutable bits1stTime = bitsInByte - _bitIndex; //what's left of this byte 122 | immutable bits2ndTime = (_bitIndex + bits) - bitsInByte; //bits to read from next byte 123 | immutable value1 = readBitsHelper(bits1stTime); 124 | _bitIndex = 0; 125 | _currentByte = this.value!ubyte; 126 | immutable value2 = readBitsHelper(bits2ndTime); 127 | return (value1 << bits2ndTime) | value2; 128 | } 129 | 130 | _bitIndex += bits; 131 | 132 | auto shift = _currentByte >> (bitsInByte - _bitIndex); 133 | return shift & (0xff >> (bitsInByte - bits)); 134 | } 135 | 136 | void setBytes(T)(in T[] bytes) @trusted if(isNumeric!T) { 137 | static if(is(T == ubyte)) { 138 | _bytes = bytes; 139 | } else { 140 | foreach(b; bytes) _bytes ~= cast(ubyte)b; 141 | } 142 | 143 | _originalBytes = _bytes; 144 | } 145 | 146 | // static assert(isCereal!Decerealiser); 147 | // static assert(isDecerealiser!Decerealiser); 148 | } 149 | -------------------------------------------------------------------------------- /src/cerealed/decerealizer.d: -------------------------------------------------------------------------------- 1 | module cerealed.decerealizer; 2 | public import cerealed.decerealiser; 3 | alias Decerealizer = Decerealiser; 4 | alias decerealize = decerealise; 5 | -------------------------------------------------------------------------------- /src/cerealed/package.d: -------------------------------------------------------------------------------- 1 | module cerealed; 2 | 3 | public import cerealed.cereal; 4 | public import cerealed.cerealiser; 5 | public import cerealed.decerealiser; 6 | public import cerealed.cerealizer; 7 | public import cerealed.decerealizer; 8 | public import cerealed.traits; 9 | public import cerealed.range; 10 | public import cerealed.utils; 11 | public import cerealed.attrs; 12 | -------------------------------------------------------------------------------- /src/cerealed/range.d: -------------------------------------------------------------------------------- 1 | module cerealed.range; 2 | 3 | import std.range; 4 | 5 | template isCerealiserRange(R) { 6 | enum isCerealiserRange = isOutputRange!(R, ubyte) && 7 | is(typeof(() { auto r = R(); r.clear(); const(ubyte)[] d = r.data; })); 8 | } 9 | 10 | 11 | struct DynamicArrayRange { 12 | void put(in ubyte val) nothrow @safe { 13 | _bytes ~= val; 14 | } 15 | 16 | void put(in ubyte[] val) nothrow @safe { 17 | _bytes ~= val; 18 | } 19 | 20 | const(ubyte)[] data() pure const nothrow @property @safe { 21 | return _bytes; 22 | } 23 | 24 | void clear() @trusted { 25 | if(_bytes !is null) { 26 | _bytes = _bytes[0..0]; 27 | _bytes.assumeSafeAppend(); 28 | } 29 | } 30 | 31 | private: 32 | ubyte[] _bytes; 33 | static assert(isCerealiserRange!DynamicArrayRange); 34 | } 35 | 36 | struct ScopeBufferRange { 37 | import cerealed.scopebuffer; //change to std.internal scopebuffer in the future 38 | ScopeBuffer!ubyte sbuf; 39 | 40 | alias sbuf this; 41 | 42 | this(ubyte[] buf) @trusted { 43 | sbuf = ScopeBuffer!ubyte(buf); 44 | } 45 | 46 | ~this() { 47 | free; 48 | } 49 | 50 | const(ubyte)[] data() pure const nothrow @property @trusted { 51 | return sbuf[]; 52 | } 53 | 54 | void clear() @trusted { 55 | sbuf.length = 0; 56 | } 57 | 58 | void free() @trusted { 59 | sbuf.free(); 60 | } 61 | 62 | static assert(isCerealiserRange!ScopeBufferRange); 63 | } 64 | -------------------------------------------------------------------------------- /src/cerealed/scopebuffer.d: -------------------------------------------------------------------------------- 1 | /* 2 | Copied from the new std.internal.scopebuffer from Phobos. 3 | The only change I made was the name of the module (so the compiler 4 | would compile it), and making opSlice inout so I could use it 5 | from const functions. 6 | */ 7 | 8 | /* 9 | * Copyright: 2014 by Digital Mars 10 | * License: $(LINK2 http://boost.org/LICENSE_1_0.txt, Boost License 1.0). 11 | * Authors: Walter Bright 12 | * Source: $(PHOBOSSRC std/internal/_scopebuffer.d) 13 | */ 14 | 15 | module cerealed.scopebuffer; 16 | 17 | 18 | //debug=ScopeBuffer; 19 | 20 | private import core.exception; 21 | private import core.stdc.stdlib : realloc; 22 | private import std.traits; 23 | 24 | /************************************** 25 | * ScopeBuffer encapsulates using a local array as a temporary buffer. 26 | * It is initialized with the local array that should be large enough for 27 | * most uses. If the need exceeds the size, ScopeBuffer will resize it 28 | * using malloc() and friends. 29 | * 30 | * ScopeBuffer cannot contain more than (uint.max-16)/2 elements. 31 | * 32 | * ScopeBuffer is an OutputRange. 33 | * 34 | * Since ScopeBuffer potentially stores elements of type T in malloc'd memory, 35 | * those elements are not scanned when the GC collects. This can cause 36 | * memory corruption. Do not use ScopeBuffer when elements of type T point 37 | * to the GC heap. 38 | * 39 | * Example: 40 | --- 41 | import core.stdc.stdio; 42 | import std.internal.scopebuffer; 43 | void main() 44 | { 45 | char[2] buf = void; 46 | auto textbuf = ScopeBuffer!char(buf); 47 | scope(exit) textbuf.free(); // necessary for cleanup 48 | 49 | // Put characters and strings into textbuf, verify they got there 50 | textbuf.put('a'); 51 | textbuf.put('x'); 52 | textbuf.put("abc"); 53 | assert(textbuf.length == 5); 54 | assert(textbuf[1..3] == "xa"); 55 | assert(textbuf[3] == 'b'); 56 | 57 | // Can shrink it 58 | textbuf.length = 3; 59 | assert(textbuf[0..textbuf.length] == "axa"); 60 | assert(textbuf[textbuf.length - 1] == 'a'); 61 | assert(textbuf[1..3] == "xa"); 62 | 63 | textbuf.put('z'); 64 | assert(textbuf[] == "axaz"); 65 | 66 | // Can shrink it to 0 size, and reuse same memory 67 | textbuf.length = 0; 68 | } 69 | --- 70 | * It is invalid to access ScopeBuffer's contents when ScopeBuffer goes out of scope. 71 | * Hence, copying the contents are necessary to keep them around: 72 | --- 73 | import std.internal.scopebuffer; 74 | string cat(string s1, string s2) 75 | { 76 | char[10] tmpbuf = void; 77 | auto textbuf = ScopeBuffer!char(tmpbuf); 78 | scope(exit) textbuf.free(); 79 | textbuf.put(s1); 80 | textbuf.put(s2); 81 | textbuf.put("even more"); 82 | return textbuf[].idup; 83 | } 84 | --- 85 | * ScopeBuffer is intended for high performance usages in $(D @system) and $(D @trusted) code. 86 | * It is designed to fit into two 64 bit registers, again for high performance use. 87 | * If used incorrectly, memory leaks and corruption can result. Be sure to use 88 | * $(D scope(exit) textbuf.free();) for proper cleanup, and do not refer to a ScopeBuffer 89 | * instance's contents after $(D ScopeBuffer.free()) has been called. 90 | * 91 | * The realloc parameter defaults to C's realloc(). Another can be supplied to override it. 92 | * 93 | * ScopeBuffer instances may be copied, as in: 94 | --- 95 | textbuf = doSomething(textbuf, args); 96 | --- 97 | * which can be very efficent, but these must be regarded as a move rather than a copy. 98 | * Additionally, the code between passing and returning the instance must not throw 99 | * exceptions, otherwise when ScopeBuffer.free() is called, memory may get corrupted. 100 | */ 101 | 102 | @system 103 | struct ScopeBuffer(T, alias realloc = /*core.stdc.stdlib*/.realloc) 104 | if (isAssignable!T && 105 | !hasElaborateDestructor!T && 106 | !hasElaborateCopyConstructor!T && 107 | !hasElaborateAssign!T) 108 | { 109 | import core.stdc.string : memcpy; 110 | 111 | /************************** 112 | * Initialize with buf to use as scratch buffer space. 113 | * Params: 114 | * buf = Scratch buffer space, must have length that is even 115 | * Example: 116 | * --- 117 | * ubyte[10] tmpbuf = void; 118 | * auto sbuf = ScopeBuffer!ubyte(tmpbuf); 119 | * --- 120 | * If buf was created by the same realloc passed as a parameter 121 | * to ScopeBuffer, then the contents of ScopeBuffer can be extracted without needing 122 | * to copy them, and ScopeBuffer.free() will not need to be called. 123 | */ 124 | this(T[] buf) 125 | in 126 | { 127 | assert(!(buf.length & wasResized)); // assure even length of scratch buffer space 128 | assert(buf.length <= uint.max); // because we cast to uint later 129 | } 130 | body 131 | { 132 | this.buf = buf.ptr; 133 | this.bufLen = cast(uint)buf.length; 134 | } 135 | 136 | unittest 137 | { 138 | ubyte[10] tmpbuf = void; 139 | auto sbuf = ScopeBuffer!ubyte(tmpbuf); 140 | } 141 | 142 | /************************** 143 | * Releases any memory used. 144 | * This will invalidate any references returned by the [] operator. 145 | * A destructor is not used, because that would make it not POD 146 | * (Plain Old Data) and it could not be placed in registers. 147 | */ 148 | void free() 149 | { 150 | debug(ScopeBuffer) buf[0 .. bufLen] = 0; 151 | if (bufLen & wasResized) 152 | realloc(buf, 0); 153 | buf = null; 154 | bufLen = 0; 155 | used = 0; 156 | } 157 | 158 | /**************************** 159 | * Copying of ScopeBuffer is not allowed. 160 | */ 161 | //@disable this(this); 162 | 163 | /************************ 164 | * Append element c to the buffer. 165 | * This member function makes ScopeBuffer an OutputRange. 166 | */ 167 | void put(T c) 168 | { 169 | /* j will get enregistered, while used will not because resize() may change used 170 | */ 171 | const j = used; 172 | if (j == bufLen) 173 | { 174 | assert(j <= (uint.max - 16) / 2); 175 | resize(j * 2 + 16); 176 | } 177 | buf[j] = c; 178 | used = j + 1; 179 | } 180 | 181 | /************************ 182 | * Append array s to the buffer. 183 | * 184 | * If $(D const(T)) can be converted to $(D T), then put will accept 185 | * $(D const(T)[]) as input. It will accept a $(D T[]) otherwise. 186 | */ 187 | private alias CT = Select!(is(const(T) : T), const(T), T); 188 | /// ditto 189 | void put(CT[] s) 190 | { 191 | const newlen = used + s.length; 192 | assert((cast(ulong)used + s.length) <= uint.max); 193 | const len = bufLen; 194 | if (newlen > len) 195 | { 196 | assert(len <= uint.max / 2); 197 | resize(newlen <= len * 2 ? len * 2 : newlen); 198 | } 199 | buf[used .. newlen] = s[]; 200 | used = cast(uint)newlen; 201 | } 202 | 203 | /****** 204 | * Retrieve a slice into the result. 205 | * Returns: 206 | * A slice into the temporary buffer that is only 207 | * valid until the next put() or ScopeBuffer goes out of scope. 208 | */ 209 | @system T[] opSlice(size_t lower, size_t upper) 210 | in 211 | { 212 | assert(lower <= bufLen); 213 | assert(upper <= bufLen); 214 | assert(lower <= upper); 215 | } 216 | body 217 | { 218 | return buf[lower .. upper]; 219 | } 220 | 221 | /// ditto 222 | @system inout(T)[] opSlice() inout 223 | { 224 | assert(used <= bufLen); 225 | return buf[0 .. used]; 226 | } 227 | 228 | /******* 229 | * Returns: 230 | * the element at index i. 231 | */ 232 | ref T opIndex(size_t i) 233 | { 234 | assert(i < bufLen); 235 | return buf[i]; 236 | } 237 | 238 | /*** 239 | * Returns: 240 | * the number of elements in the ScopeBuffer 241 | */ 242 | @property size_t length() const 243 | { 244 | return used; 245 | } 246 | 247 | /*** 248 | * Used to shrink the length of the buffer, 249 | * typically to 0 so the buffer can be reused. 250 | * Cannot be used to extend the length of the buffer. 251 | */ 252 | @property void length(size_t i) 253 | in 254 | { 255 | assert(i <= this.used); 256 | } 257 | body 258 | { 259 | this.used = cast(uint)i; 260 | } 261 | 262 | alias opDollar = length; 263 | 264 | private: 265 | T* buf; 266 | // Using uint instead of size_t so the struct fits in 2 registers in 64 bit code 267 | uint bufLen; 268 | enum wasResized = 1; // this bit is set in bufLen if we control the memory 269 | uint used; 270 | 271 | void resize(size_t newsize) 272 | in 273 | { 274 | assert(newsize <= uint.max); 275 | } 276 | body 277 | { 278 | //writefln("%s: oldsize %s newsize %s", id, buf.length, newsize); 279 | newsize |= wasResized; 280 | void *newBuf = realloc((bufLen & wasResized) ? buf : null, newsize * T.sizeof); 281 | if (!newBuf) 282 | core.exception.onOutOfMemoryError(); 283 | if (!(bufLen & wasResized)) 284 | { 285 | memcpy(newBuf, buf, used * T.sizeof); 286 | debug(ScopeBuffer) buf[0 .. bufLen] = 0; 287 | } 288 | buf = cast(T*)newBuf; 289 | bufLen = cast(uint)newsize; 290 | 291 | /* This function is called only rarely, 292 | * inlining results in poorer register allocation. 293 | */ 294 | version (DigitalMars) 295 | /* With dmd, a fake loop will prevent inlining. 296 | * Using a hack until a language enhancement is implemented. 297 | */ 298 | while (1) { break; } 299 | } 300 | } 301 | 302 | unittest 303 | { 304 | import core.stdc.stdio; 305 | import std.range; 306 | 307 | char[2] tmpbuf = void; 308 | { 309 | // Exercise all the lines of code except for assert(0)'s 310 | auto textbuf = ScopeBuffer!char(tmpbuf); 311 | scope(exit) textbuf.free(); 312 | 313 | static assert(isOutputRange!(ScopeBuffer!char, char)); 314 | 315 | textbuf.put('a'); 316 | textbuf.put('x'); 317 | textbuf.put("abc"); // tickle put([])'s resize 318 | assert(textbuf.length == 5); 319 | assert(textbuf[1..3] == "xa"); 320 | assert(textbuf[3] == 'b'); 321 | 322 | textbuf.length = textbuf.length - 1; 323 | assert(textbuf[0..textbuf.length] == "axab"); 324 | 325 | textbuf.length = 3; 326 | assert(textbuf[0..textbuf.length] == "axa"); 327 | assert(textbuf[textbuf.length - 1] == 'a'); 328 | assert(textbuf[1..3] == "xa"); 329 | 330 | textbuf.put(cast(dchar)'z'); 331 | assert(textbuf[] == "axaz"); 332 | 333 | textbuf.length = 0; // reset for reuse 334 | assert(textbuf.length == 0); 335 | 336 | foreach (char c; "asdf;lasdlfaklsdjfalksdjfa;lksdjflkajsfdasdfkja;sdlfj") 337 | { 338 | textbuf.put(c); // tickle put(c)'s resize 339 | } 340 | assert(textbuf[] == "asdf;lasdlfaklsdjfalksdjfa;lksdjflkajsfdasdfkja;sdlfj"); 341 | } // run destructor on textbuf here 342 | 343 | } 344 | 345 | unittest 346 | { 347 | string cat(string s1, string s2) 348 | { 349 | char[10] tmpbuf = void; 350 | auto textbuf = ScopeBuffer!char(tmpbuf); 351 | scope(exit) textbuf.free(); 352 | textbuf.put(s1); 353 | textbuf.put(s2); 354 | textbuf.put("even more"); 355 | return textbuf[].idup; 356 | } 357 | 358 | auto s = cat("hello", "betty"); 359 | assert(s == "hellobettyeven more"); 360 | } 361 | 362 | /********************************* 363 | * This is a slightly simpler way to create a ScopeBuffer instance 364 | * that uses type deduction. 365 | * Params: 366 | * tmpbuf = the initial buffer to use 367 | * Returns: 368 | * an instance of ScopeBuffer 369 | * Example: 370 | --- 371 | ubyte[10] tmpbuf = void; 372 | auto sb = scopeBuffer(tmpbuf); 373 | scope(exit) sp.free(); 374 | --- 375 | */ 376 | 377 | auto scopeBuffer(T)(T[] tmpbuf) 378 | { 379 | return ScopeBuffer!T(tmpbuf); 380 | } 381 | 382 | unittest 383 | { 384 | ubyte[10] tmpbuf = void; 385 | auto sb = scopeBuffer(tmpbuf); 386 | scope(exit) sb.free(); 387 | } 388 | 389 | unittest 390 | { 391 | ScopeBuffer!(int*) b; 392 | int*[] s; 393 | b.put(s); 394 | 395 | ScopeBuffer!char c; 396 | string s1; 397 | char[] s2; 398 | c.put(s1); 399 | c.put(s2); 400 | } 401 | -------------------------------------------------------------------------------- /src/cerealed/traits.d: -------------------------------------------------------------------------------- 1 | module cerealed.traits; 2 | 3 | import cerealed.cereal; 4 | import cerealed.cerealiser; 5 | import cerealed.decerealiser; 6 | 7 | 8 | void checkCereal(T)() { 9 | ubyte b; 10 | auto cereal = T.init; 11 | cereal.grainUByte(b); 12 | uint val; 13 | cereal.grainBits(val, 3); 14 | CerealType type = cereal.type; 15 | // can't check grainClass because it calls grainClassImpl, 16 | // which uses the template constraint isCereal 17 | // Class class_; 18 | // cereal.grainClass(class_); 19 | } 20 | 21 | enum isCereal(T) = is(typeof(checkCereal!T)); 22 | 23 | void checkCerealiser(T)() { 24 | checkCereal!T; 25 | static assert(T.type == CerealType.WriteBytes); 26 | } 27 | 28 | enum isCerealiser(T) = is(typeof(checkCerealiser!T)); 29 | 30 | void checkDecerealiser(T)() { 31 | checkCereal!T; 32 | static assert(T.type == CerealType.ReadBytes); 33 | auto dec = T(); 34 | ulong bl = dec.bytesLeft; 35 | } 36 | 37 | enum isDecerealiser(T) = is(typeof(checkDecerealiser!T)); 38 | 39 | 40 | bool hasFunc(T, string funcName)() { 41 | if(!__ctfe) { 42 | auto obj = T.init; 43 | auto enc = Cerealiser(); 44 | mixin("obj." ~ funcName ~ "(enc);"); 45 | auto dec = Decerealiser(); 46 | mixin("obj." ~ funcName ~ "(dec);"); 47 | } 48 | return true; 49 | } 50 | 51 | enum hasAccept(T) = hasFunc!(T, "accept"); 52 | enum hasPostBlit(T) = hasFunc!(T, "postBlit"); 53 | enum hasPreBlit(T) = hasFunc!(T, "preBlit"); 54 | 55 | unittest { 56 | struct Accept { 57 | void accept(C)(auto ref C cereal) if(isCereal!C) { } 58 | } 59 | 60 | static assert(hasAccept!Accept); 61 | } 62 | 63 | mixin template assertHas(T, string funcName) { 64 | static assert(hasFunc!(T, funcName)); 65 | } 66 | 67 | mixin template assertHasPostBlit(T) { mixin assertHas!(T, "postBlit"); } 68 | mixin template assertHasPreBlit(T) { mixin assertHas!(T, "preBlit"); } 69 | mixin template assertHasAccept(T) { mixin assertHas!(T, "accept"); } 70 | 71 | private class Class { ubyte ub; } 72 | -------------------------------------------------------------------------------- /src/cerealed/utils.d: -------------------------------------------------------------------------------- 1 | module cerealed.utils; 2 | 3 | import std.traits; 4 | 5 | int unalignedSizeof(T)() { 6 | T initValue() { 7 | static if(is(T == class)) 8 | return new T; 9 | else 10 | return T.init; 11 | } 12 | 13 | static if(!isAggregateType!T || is(T == union)) { 14 | return T.sizeof; 15 | } else { 16 | int size; 17 | foreach(member; initValue.tupleof) { 18 | size += unalignedSizeof!(typeof(member)); 19 | } 20 | return size; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/bugs.d: -------------------------------------------------------------------------------- 1 | module tests.bugs; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | 6 | 7 | private struct Pair { 8 | string s; 9 | int i; 10 | } 11 | 12 | 13 | void testAssocArrayWithPair() { 14 | auto p = Pair("foo", 5); 15 | auto map = [p: 105]; 16 | auto enc = Cerealiser(); 17 | 18 | enc ~= map; 19 | enc.bytes.shouldEqual([0, 1, 0, 3, 'f', 'o', 'o', 0, 0, 0, 5, 0, 0, 0, 105]); 20 | 21 | auto dec = Decerealiser(enc.bytes); 22 | auto map2 = dec.value!(int[Pair]); 23 | 24 | map.keys.shouldEqual(map2.keys); 25 | map.values.shouldEqual(map2.values); 26 | } 27 | 28 | void testByteArray() { 29 | ubyte[] arr = [1,2,3,4]; 30 | 31 | auto enc = Cerealiser(); 32 | enc ~= arr; 33 | 34 | auto dec = Decerealiser(enc.bytes); 35 | auto arr2 = dec.value!(ubyte[]); 36 | 37 | arr.shouldEqual(arr2); 38 | } 39 | 40 | 41 | @("bool array") 42 | unittest { 43 | auto arr = [true, false, true]; 44 | auto enc = Cerealiser(); 45 | enc ~= arr; 46 | enc.bytes.shouldEqual([0, 3, 1, 0, 1]); 47 | 48 | auto dec = Decerealiser(enc.bytes); 49 | dec.value!(bool[]).shouldEqual(arr); 50 | } 51 | 52 | @("19") 53 | unittest { 54 | static struct Foo { string value; } 55 | 56 | auto data = Foo("bar").cerealise; 57 | auto f1 = data.decerealise!Foo; 58 | f1.value.should == "bar"; 59 | 60 | auto data2 = Foo("baz").cerealise; 61 | data[] = data2[]; 62 | auto f2 = data.decerealise!Foo; 63 | 64 | f1.value.should == "bar"; 65 | f2.value.should == "baz"; 66 | } 67 | -------------------------------------------------------------------------------- /tests/cerealiser_impl.d: -------------------------------------------------------------------------------- 1 | module tests.cerealiser_impl; 2 | import unit_threaded; 3 | import cerealed; 4 | import core.exception; 5 | 6 | 7 | struct WhateverStruct { 8 | ushort i; 9 | string s; 10 | } 11 | 12 | void testOldCerealiser() { 13 | auto enc = DynamicArrayCerealiser(); 14 | enc ~= WhateverStruct(5, "blargh"); 15 | enc.bytes.shouldEqual([ 0, 5, 0, 6, 'b', 'l', 'a', 'r', 'g', 'h' ]); 16 | enc.reset(); 17 | enc.bytes.shouldBeEmpty; 18 | (enc ~= 4).shouldNotThrow!RangeError; 19 | } 20 | 21 | void testScopeBufferCerealiser() { 22 | ubyte[32] buf = void; 23 | 24 | writelnUt("Creating the range"); 25 | auto sbufRange = ScopeBufferRange(buf); 26 | 27 | scope(exit) sbufRange.free(); 28 | 29 | writelnUt("Creating the cerealiser"); 30 | auto enc = CerealiserImpl!ScopeBufferRange(sbufRange); 31 | 32 | enc ~= WhateverStruct(5, "blargh"); 33 | enc.bytes.shouldEqual([ 0, 5, 0, 6, 'b', 'l', 'a', 'r', 'g', 'h' ]); 34 | } 35 | 36 | 37 | void testCerealise() { 38 | WhateverStruct(5, "blargh").cerealise!(bytes => bytes.shouldEqual([0, 5, 0, 6, 'b', 'l', 'a', 'r', 'g', 'h'])); 39 | } 40 | -------------------------------------------------------------------------------- /tests/classes.d: -------------------------------------------------------------------------------- 1 | module tests.classes; 2 | 3 | import unit_threaded; 4 | import cerealed.cerealiser; 5 | import cerealed.decerealiser; 6 | 7 | private class DummyClass { 8 | int i; 9 | bool opEquals(DummyClass d) const pure nothrow { 10 | return false; 11 | } 12 | } 13 | 14 | void testDummyClass() { 15 | auto enc = Cerealiser(); 16 | auto dummy = new DummyClass(); 17 | enc ~= dummy; 18 | } 19 | 20 | private struct DummyStruct { 21 | ubyte first; 22 | ushort second; 23 | } 24 | 25 | private class ClassWithStruct { 26 | DummyStruct dummy; 27 | ubyte anotherByte; 28 | this() { 29 | } 30 | this(DummyStruct d, ubyte a) { 31 | dummy = d; 32 | anotherByte = a; 33 | } 34 | override string toString() const { //so it can be used in shouldEqual 35 | import std.conv; 36 | return text("ClassWithStruct(", dummy, ", ", anotherByte, ")"); 37 | } 38 | } 39 | 40 | void testClassWithStruct() { 41 | auto enc = Cerealiser(); 42 | auto klass = new ClassWithStruct(DummyStruct(2, 3), 4); 43 | enc ~= klass; 44 | const bytes = [2, 0, 3, 4]; 45 | enc.bytes.shouldEqual(bytes); 46 | 47 | auto dec = Decerealiser(bytes); 48 | dec.value!ClassWithStruct.shouldEqual(klass); 49 | } 50 | 51 | class BaseClass { 52 | ubyte byte1; 53 | ubyte byte2; 54 | this(ubyte byte1, ubyte byte2) { 55 | this.byte1 = byte1; 56 | this.byte2 = byte2; 57 | } 58 | } 59 | 60 | class DerivedClass: BaseClass { 61 | ubyte byte3; 62 | ubyte byte4; 63 | this() { //needed for deserialisation 64 | this(0, 0, 0, 0); 65 | } 66 | this(ubyte byte1, ubyte byte2, ubyte byte3, ubyte byte4) { 67 | super(byte1, byte2); 68 | this.byte3 = byte3; 69 | this.byte4 = byte4; 70 | } 71 | override string toString() const { //so it can be used in shouldEqual 72 | import std.conv; 73 | return text("DerivedClass(", byte1, ", ", byte2, ", ", byte2, ", ", byte4, ")"); 74 | } 75 | } 76 | 77 | void testDerivedClass() { 78 | auto enc = Cerealiser(); 79 | auto klass = new DerivedClass(2, 4, 8, 9); 80 | enc ~= klass; 81 | const bytes = [2, 4, 8, 9]; 82 | enc.bytes.shouldEqual(bytes); 83 | 84 | auto dec = Decerealiser(bytes); 85 | dec.value!DerivedClass.shouldEqual(klass); 86 | } 87 | 88 | 89 | void testSerialisationViaBaseClass() { 90 | BaseClass klass = new DerivedClass(2, 4, 8, 9); 91 | const baseBytes = [2, 4]; 92 | const childBytes = [2, 4, 8, 9]; 93 | 94 | auto enc = Cerealiser(); 95 | enc ~= klass; 96 | enc.bytes.shouldEqual(baseBytes); 97 | 98 | Cerealiser.registerChildClass!DerivedClass; 99 | enc.reset(); 100 | enc ~= klass; 101 | enc.bytes.shouldEqual(childBytes); 102 | 103 | auto dec = Decerealiser(childBytes); 104 | dec.value!DerivedClass.shouldEqual(cast(DerivedClass)klass); 105 | } 106 | -------------------------------------------------------------------------------- /tests/compile_time.d: -------------------------------------------------------------------------------- 1 | module tests.compile_time; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | 6 | private struct WrongStruct1 { 7 | @Bits!9 ubyte b; 8 | } 9 | 10 | private struct WrongStruct2 { 11 | @Bits!17 ushort b; 12 | } 13 | 14 | private struct RightStruct { 15 | @Bits!8 ubyte ub; 16 | @Bits!16 ushort us; 17 | @Bits!32 uint ui; 18 | } 19 | 20 | void testBitsTooBig() { 21 | static assert(!is(typeof(() { auto c = Cerealiser(); c ~= WrongStruct1(3); }))); 22 | static assert(!is(typeof(() { auto c = Cerealiser(); c ~= WrongStruct2(3); }))); 23 | static assert(is(typeof(() { auto c = Cerealiser(); c ~= RightStruct(3); }))); 24 | } 25 | -------------------------------------------------------------------------------- /tests/decode.d: -------------------------------------------------------------------------------- 1 | module tests.decode; 2 | 3 | import unit_threaded; 4 | import cerealed.decerealiser; 5 | import core.exception; 6 | 7 | void testDecodeBool() { 8 | import cerealed.cereal: grain; 9 | auto cereal = Decerealiser([1, 0, 1, 0, 0, 1]); 10 | bool val; 11 | cereal.grain(val); shouldEqual(val, true); 12 | cereal.grain(val); shouldEqual(val, false); 13 | cereal.grain(val); shouldEqual(val, true); 14 | cereal.grain(val); shouldEqual(val, false); 15 | cereal.grain(val); shouldEqual(val, false); 16 | cereal.grain(val); shouldEqual(val, true); 17 | cereal.value!bool.shouldThrow!RangeError; //no more bytes 18 | } 19 | 20 | 21 | void testDecodeByte() { 22 | auto cereal = Decerealiser([0x0, 0x2, 0xfc]); 23 | cereal.value!byte.shouldEqual(0); 24 | cereal.value!byte.shouldEqual(2); 25 | cereal.value!byte.shouldEqual(-4); 26 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 27 | } 28 | 29 | void testDecodeRefByte() { 30 | import cerealed.cereal: grain; 31 | auto cereal = Decerealiser([0xfc]); 32 | byte val; 33 | cereal.grain(val); 34 | val.shouldEqual(-4); 35 | } 36 | 37 | void testDecodeUByte() { 38 | auto cereal = Decerealiser([0x0, 0x2, 0xfc]); 39 | cereal.value!ubyte.shouldEqual(0); 40 | cereal.value!ubyte.shouldEqual(2); 41 | cereal.value!ubyte.shouldEqual(252); 42 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 43 | } 44 | 45 | void testDecodeShort() { 46 | auto cereal = Decerealiser([0xff, 0xfe, 0x0, 0x3]); 47 | cereal.value!short.shouldEqual(-2); 48 | cereal.value!short.shouldEqual(3); 49 | shouldThrow!RangeError(cereal.value!short); //no more bytes 50 | } 51 | 52 | void testDecodeRefShort() { 53 | import cerealed.cereal: grain; 54 | auto cereal = Decerealiser([0xff, 0xfe]); 55 | short val; 56 | cereal.grain(val); 57 | val.shouldEqual(-2); 58 | } 59 | 60 | void testDecodeInt() { 61 | auto cereal = Decerealiser([ 0xff, 0xf0, 0xbd, 0xc0]); 62 | cereal.value!int.shouldEqual(-1_000_000); 63 | shouldThrow!RangeError(cereal.value!int); //no more bytes 64 | } 65 | 66 | void testDecodeRefInt() { 67 | import cerealed.cereal: grain; 68 | auto cereal = Decerealiser([0xff, 0xf0, 0xbd, 0xc0]); 69 | int val; 70 | cereal.grain(val); 71 | val.shouldEqual(-1_000_000); 72 | } 73 | 74 | void testDecodeLong() { 75 | auto cereal = Decerealiser([ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2]); 76 | cereal.value!long.shouldEqual(1); 77 | cereal.value!long.shouldEqual(2); 78 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 79 | } 80 | 81 | void testDecodeRefLong() { 82 | import cerealed.cereal: grain; 83 | auto cereal = Decerealiser([ 0, 0, 0, 0, 0, 0, 0, 1]); 84 | long val; 85 | cereal.grain(val); 86 | val.shouldEqual(1); 87 | } 88 | 89 | 90 | void testDecodeBigULong() { 91 | auto dec = Decerealiser([0xd8, 0xbf, 0xc7, 0xcd, 0x2d, 0x9b, 0xa1, 0xb1]); 92 | dec.value!ulong.shouldEqual(0xd8bfc7cd2d9ba1b1); 93 | shouldThrow!RangeError(dec.value!ubyte); //no more bytes 94 | } 95 | 96 | 97 | void testDecodeDouble() { 98 | auto cereal = Decerealiser([ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2]); 99 | shouldNotThrow(cereal.value!double); 100 | shouldNotThrow(cereal.value!double); 101 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 102 | } 103 | 104 | void testDecodeChars() { 105 | auto cereal = Decerealiser([ 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff ]); 106 | cereal.value!char.shouldEqual(0xff); 107 | cereal.value!wchar.shouldEqual(0xffff); 108 | cereal.value!dchar.shouldEqual(0x0000ffff); 109 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 110 | } 111 | 112 | void testDecodeRefChar() { 113 | import cerealed.cereal: grain; 114 | auto cereal = Decerealiser([0xff]); 115 | char val; 116 | cereal.grain(val); 117 | val.shouldEqual(0xff); 118 | } 119 | 120 | 121 | void testDecodeArray() { 122 | auto cereal = Decerealiser([ 0, 3, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 9 ]); 123 | cereal.value!(int[]).shouldEqual([ 2, 6, 9 ]); 124 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 125 | } 126 | 127 | void testDecodeRefArray() { 128 | import cerealed.cereal: grain; 129 | auto cereal = Decerealiser([0, 1, 0, 0, 0, 2]); 130 | int[] val; 131 | cereal.grain(val); 132 | val.shouldEqual([2]); 133 | } 134 | 135 | void testDecodeArrayLongLength() { 136 | auto cereal = Decerealiser([ 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 9 ]); 137 | cereal.value!(int[], long).shouldEqual([ 2, 6, 9 ]); 138 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 139 | } 140 | 141 | void testDecodeAssocArray() { 142 | auto cereal = Decerealiser([ 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6 ]); 143 | cereal.value!(int[int]).shouldEqual([ 1:2, 3:6]); 144 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 145 | } 146 | 147 | 148 | void testDecodeRefAssocArray() { 149 | import cerealed.cereal: grain; 150 | auto cereal = Decerealiser([0, 1, 0, 0, 0, 2, 0, 0, 0, 3]); 151 | int[int] val; 152 | cereal.grain(val); 153 | val.shouldEqual([2:3]); 154 | } 155 | 156 | void testDecodeAssocArrayIntLength() { 157 | auto cereal = Decerealiser([ 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6 ]); 158 | cereal.value!(int[int], int).shouldEqual([ 1:2, 3:6]); 159 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 160 | } 161 | 162 | void testDecodeString() { 163 | auto cereal = Decerealiser([0, 5, 'a', 't', 'o', 'y', 'n']); 164 | cereal.value!(string).shouldEqual("atoyn"); 165 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 166 | } 167 | 168 | void testDecodeRefString() { 169 | import cerealed.cereal: grain; 170 | auto cereal = Decerealiser([0, 5, 'a', 't', 'o', 'y', 'n']); 171 | string val; 172 | cereal.grain(val); 173 | val.shouldEqual("atoyn"); 174 | cereal.value!ubyte.shouldThrow!RangeError; //no more bytes 175 | } 176 | 177 | void testDecodeBits() { 178 | auto cereal = Decerealiser([ 0x9e, 0xea]); 179 | //1001 1110 1110 1010 or 180 | //100 111 10111 01 010 181 | cereal.readBits(3).shouldEqual(4); 182 | cereal.readBits(3).shouldEqual(7); 183 | cereal.readBits(5).shouldEqual(23); 184 | cereal.readBits(2).shouldEqual(1); 185 | cereal.readBits(3).shouldEqual(2); 186 | 187 | cereal.reset(); 188 | cereal.readBits(3).shouldEqual(4); 189 | cereal.readBits(3).shouldEqual(7); 190 | cereal.readBits(5).shouldEqual(23); 191 | cereal.readBits(2).shouldEqual(1); 192 | cereal.readBits(3).shouldEqual(2); 193 | } 194 | 195 | void testDecodeBitsMultiByte() { 196 | auto cereal = Decerealiser([ 0x9e, 0xea]); 197 | cereal.readBits(9).shouldEqual(317); 198 | cereal.readBits(7).shouldEqual(0x6a); 199 | } 200 | 201 | void testDecodeStringArray() { 202 | auto dec = Decerealiser([ 0, 3, 203 | 0, 3, 'f', 'o', 'o', 204 | 0, 4, 'w', '0', '0', 't', 205 | 0, 2, 'l', 'i']); 206 | dec.value!(string[]).shouldEqual(["foo", "w00t", "li"]); 207 | } 208 | 209 | @("Decode enum with bad value") 210 | unittest { 211 | struct Foo { 212 | enum Type { 213 | foo, 214 | bar, 215 | } 216 | Type type; 217 | } 218 | 219 | Decerealiser([0, 0, 0, 42]).value!(Foo.Type).shouldThrow; 220 | Decerealiser([0, 0, 0, 42]).value!Foo.shouldThrow; 221 | } 222 | 223 | 224 | @("Throws if length of array is larger than packet") 225 | unittest { 226 | Decerealiser([0, 8, 1, 2]).value!(ubyte[]).shouldThrowWithMessage( 227 | "Not enough bytes left to decerealise ubyte[] of 8 elements\n" ~ 228 | "Bytes left: 2, Needed: 8, bytes: [1, 2]"); 229 | 230 | Decerealiser([0, 8, 1, 2]).value!(int[]).shouldThrowWithMessage( 231 | "Not enough bytes left to decerealise int[] of 8 elements\n" ~ 232 | "Bytes left: 2, Needed: 32, bytes: [1, 2]"); 233 | } 234 | 235 | @("Types with @disable this can be encoded/decoded") 236 | @safe unittest { 237 | static struct NoDefault { 238 | ubyte i; 239 | @disable this(); 240 | this(ubyte i) { this.i = i; } 241 | } 242 | 243 | [5].decerealise!NoDefault.shouldEqual(NoDefault(5)); 244 | } 245 | -------------------------------------------------------------------------------- /tests/encode.d: -------------------------------------------------------------------------------- 1 | module tests.encode; 2 | 3 | import unit_threaded; 4 | import cerealed.cerealiser; 5 | 6 | 7 | void testEncodeBool() { 8 | auto cereal = Cerealiser(); 9 | cereal.write(false); 10 | cereal.write(true); 11 | cereal.write(false); 12 | cereal.bytes.shouldEqual([ 0x0, 0x1, 0x0 ]); 13 | 14 | cereal ~= true; 15 | cereal.bytes.shouldEqual([ 0x0, 0x1, 0x0, 0x1 ]); 16 | } 17 | 18 | void testEncodeByte() { 19 | auto cereal = Cerealiser(); 20 | byte[] ins = [ 1, 3, -2, 5, -4]; 21 | foreach(i; ins) cereal.write(i); 22 | cereal.bytes.shouldEqual([ 0x1, 0x3, 0xfe, 0x5, 0xfc ]); 23 | } 24 | 25 | void testEncodeUByte() { 26 | auto cereal = Cerealiser(); 27 | ubyte[] ins = [ 2, 3, 12, 10]; 28 | foreach(i; ins) cereal ~= i; 29 | cereal.bytes.shouldEqual([ 0x2, 0x3, 0xc, 0xa ]); 30 | } 31 | 32 | void testEncodeShort() { 33 | auto cereal = Cerealiser(); 34 | short[] ins = [ -2, 3, -32767, 0]; 35 | foreach(i; ins) cereal ~= i; 36 | cereal.bytes.shouldEqual([ 0xff, 0xfe, 0x0, 0x3, 0x80, 0x01, 0x0, 0x0 ]); 37 | } 38 | 39 | void testEncodeUShort() { 40 | auto cereal = Cerealiser(); 41 | ushort[] ins = [ 2, 3, cast(short)65535, 0]; 42 | foreach(i; ins) cereal ~= i; 43 | cereal.bytes.shouldEqual([ 0x0, 0x2, 0x0, 0x3, 0xff, 0xff, 0x0, 0x0 ]); 44 | } 45 | 46 | void testEncodeInt() { 47 | auto cereal = Cerealiser(); 48 | int[] ins = [ 3, -1_000_000]; 49 | foreach(i; ins) cereal ~= i; 50 | cereal.bytes.shouldEqual([ 0x0, 0x0, 0x0, 0x3, 0xff, 0xf0, 0xbd, 0xc0 ]); 51 | } 52 | 53 | void testEncodeUInt() { 54 | auto cereal = Cerealiser(); 55 | uint[] ins = [ 1_000_000, 0]; 56 | foreach(i; ins) cereal ~= i; 57 | cereal.bytes.shouldEqual([ 0x0, 0x0f, 0x42, 0x40, 0x0, 0x0, 0x0, 0x0 ]); 58 | } 59 | 60 | void testEncodeLong() { 61 | auto cereal = Cerealiser(); 62 | long[] ins = [1, 2]; 63 | foreach(i; ins) cereal ~= i; 64 | cereal.bytes.shouldEqual([ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2]); 65 | } 66 | 67 | void testEncodeULong() { 68 | auto cereal = Cerealiser(); 69 | cereal ~= 45L; 70 | cereal.bytes.shouldEqual([ 0, 0, 0, 0, 0, 0, 0, 45 ]); 71 | 72 | cereal = Cerealiser(); 73 | cereal ~= 42L; 74 | cereal.bytes.shouldEqual([ 0, 0, 0, 0, 0, 0, 0, 42 ]); 75 | } 76 | 77 | void testEncodeBigULong() { 78 | ulong val = 0xd8bfc7cd2d9ba1b1; 79 | auto enc = Cerealiser(); 80 | enc ~= val; 81 | enc.bytes.shouldEqual([0xd8, 0xbf, 0xc7, 0xcd, 0x2d, 0x9b, 0xa1, 0xb1]); 82 | } 83 | 84 | void testEncodeFloat() { 85 | auto cereal = Cerealiser(); 86 | cereal ~= 1.0f; 87 | } 88 | 89 | void testEncodeDouble() { 90 | auto cereal = Cerealiser(); 91 | cereal ~= 1.0; 92 | } 93 | 94 | void testEncodeChars() { 95 | auto cereal = Cerealiser(); 96 | char c; cereal ~= c; 97 | wchar w; cereal ~= w; 98 | dchar d; cereal ~= d; 99 | cereal.bytes.shouldEqual([ 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff]); 100 | } 101 | 102 | void testEncodeArray() { 103 | auto cereal = Cerealiser(); 104 | const ints = [ 2, 6, 9]; 105 | cereal ~= ints; 106 | //encoding should be a short with the length, plus payload 107 | cereal.bytes.shouldEqual([ 0, 3, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 9]); 108 | } 109 | 110 | void testEncodeAssocArray() { 111 | import std.algorithm; 112 | 113 | auto cereal = Cerealiser(); 114 | const intToInt = [ 1:2, 3:6]; 115 | import std.traits; 116 | import std.range; 117 | cereal ~= intToInt; 118 | //short with length, then payload 119 | cereal.bytes[0..2].shouldEqual([0, 2]); 120 | cereal.bytes.length.shouldEqual(18); //ushort length, 4 bytes each for each int 121 | cereal.bytes.canFind([0, 0, 0, 1, /*:*/ 0, 0, 0, 2,]).shouldBeTrue; 122 | cereal.bytes.canFind([0, 0, 0, 3, /*:*/ 0, 0, 0, 6,]).shouldBeTrue; 123 | } 124 | 125 | void testEncodeString() { 126 | auto cereal = Cerealiser(); 127 | const str = "foobarbaz"; 128 | cereal ~= str; 129 | //short with length, then payload 130 | cereal.bytes.shouldEqual([ 0, 9, 'f', 'o', 'o', 'b', 'a', 'r', 'b', 'a', 'z' ]); 131 | } 132 | 133 | void testEncodeNibble() { 134 | auto cereal = Cerealiser(); 135 | cereal.writeBits(0x4, 4); 136 | cereal.writeBits(0xf, 4); 137 | cereal.bytes.shouldEqual([ 0x4f ]); 138 | } 139 | 140 | void testEncodeSubByte() { 141 | auto cereal = Cerealiser(); 142 | cereal.writeBits(1, 1); 143 | cereal.writeBits(3, 2); 144 | cereal.writeBits(0, 1); 145 | cereal.writeBits(5, 3); 146 | cereal.writeBits(1, 1); 147 | cereal.bytes.shouldEqual([ 0xeb]); 148 | } 149 | 150 | void testEncodeSubWord() { 151 | auto cereal = Cerealiser(); 152 | cereal.writeBits(4, 3); 153 | cereal.writeBits(7, 3); 154 | cereal.writeBits(23, 5); 155 | cereal.writeBits(1, 2); 156 | cereal.writeBits(2, 3); 157 | cereal.bytes.shouldEqual([ 0x9e, 0xea]); 158 | } 159 | 160 | void testEncodeMoreThan8Bits() { 161 | { 162 | auto cereal = Cerealiser(); 163 | cereal.writeBits(1, 9); 164 | cereal.writeBits(15, 7); 165 | cereal.bytes.shouldEqual([ 0x00, 0x8f]); 166 | } 167 | { 168 | auto cereal = Cerealiser(); 169 | cereal.writeBits((0x9e << 1) | 1, 9); 170 | cereal.writeBits(0xea & 0x7f, 7); 171 | cereal.bytes.shouldEqual([ 0x9e, 0xea]); 172 | } 173 | } 174 | 175 | void testEncodeFailsIfTooBigForBits() { 176 | auto cereal = Cerealiser(); 177 | shouldNotThrow(cereal.writeBits(1, 1)); 178 | shouldThrow(cereal.writeBits(2, 1)); 179 | shouldNotThrow(cereal.writeBits(3, 2)); 180 | shouldThrow(cereal.writeBits(5, 2)); 181 | } 182 | 183 | void testEncodeTwoBytesBits() { 184 | auto cereal = Cerealiser(); 185 | immutable uint value = 5; 186 | cereal.writeBits(3, 4); 187 | cereal.writeBits(1, 1); 188 | cereal.writeBits(2, 2); 189 | cereal.writeBits(0, 1); 190 | cereal.writeBits(5, 8); 191 | cereal.bytes.shouldEqual([0x3c, 0x05]); 192 | } 193 | 194 | 195 | void testCerealise() { 196 | cerealise(4).shouldEqual([0, 0, 0, 4]); 197 | cerealize("foo").shouldEqual([0, 3, 'f', 'o', 'o']); 198 | } 199 | 200 | @("struct with no default ctor") 201 | @safe pure unittest { 202 | static struct NoDefault { 203 | ubyte i; 204 | @disable this(); 205 | this(ubyte i) { this.i = i; } 206 | } 207 | 208 | cerealise(NoDefault(5)).shouldEqual([5]); 209 | } 210 | -------------------------------------------------------------------------------- /tests/encode_decode.d: -------------------------------------------------------------------------------- 1 | module tests.encode_decode; 2 | 3 | import unit_threaded; 4 | import cerealed.cerealiser; 5 | import cerealed.decerealiser; 6 | import core.exception; 7 | 8 | 9 | private void implEncDec(T)(T[] values) { 10 | auto enc = Cerealiser(); 11 | import std.stdio; 12 | foreach(b; values) { 13 | writelnUt("Encoding ", b); 14 | enc ~= b; 15 | } 16 | auto dec = Decerealiser(enc.bytes); 17 | writelnUt("Decoding to match ", values); 18 | writelnUt("Bytes: ", enc.bytes); 19 | foreach(b; values) shouldEqual(dec.value!T, b); 20 | dec.value!ubyte.shouldThrow!RangeError; //no more bytes 21 | } 22 | 23 | void testEncDecBool() { 24 | implEncDec([ true, true, false, false, true]); 25 | } 26 | 27 | 28 | private void implEncDecValues(T, alias arr)() { 29 | T[] values = arr; 30 | implEncDec(values); 31 | } 32 | 33 | void testEncDecByte() { 34 | implEncDecValues!(byte, [ 1, 3, -2, 5, -4 ]); 35 | } 36 | 37 | void testEncDecUByte() { 38 | implEncDecValues!(ubyte, [ 1, 255, 12 ]); 39 | } 40 | 41 | void testEncDecShort() { 42 | implEncDecValues!(short, [ 1, -2, -32768, 5 ]); 43 | } 44 | 45 | void testEncDecUShort() { 46 | implEncDecValues!(short, [ 1, -2, 32767, 5 ]); 47 | } 48 | 49 | void testEncDecInt() { 50 | implEncDecValues!(int, [ 1, -2, -1_000_000, 2_000_000 ]); 51 | } 52 | 53 | void testEncDecUInt() { 54 | implEncDecValues!(uint, [ 1, -2, 1_000_000, 2_000_000 ]); 55 | } 56 | 57 | void testEncDecLong() { 58 | implEncDecValues!(long, [ 5_000_000, 2, -3, -5_000_000_000, 1 ]); 59 | } 60 | 61 | void testEncDecULong() { 62 | implEncDecValues!(ulong, [ 5_000_000, 2, 7_000_000_000, 1 ]); 63 | } 64 | 65 | void testEncDecFloat() { 66 | implEncDec([ 2.0f, -4.3f, 3.1415926f ]); //don't add a value without 'f'! 67 | } 68 | 69 | void testEncDecDouble() { 70 | implEncDec([ 2.0, -9.0 ]); 71 | } 72 | 73 | void testEncDecChars() { 74 | char c = 5; 75 | wchar w = 300; 76 | dchar d = 1_000_000; 77 | auto enc = Cerealiser(); 78 | enc ~= c; enc ~= w; enc ~= d; 79 | auto dec = Decerealiser(enc.bytes); 80 | dec.value!char.shouldEqual(c); 81 | dec.value!wchar.shouldEqual(w); 82 | dec.value!dchar.shouldEqual(d); 83 | dec.value!ubyte.shouldThrow!RangeError; //no more bytes 84 | } 85 | 86 | void testEncDecArray() { 87 | auto enc = Cerealiser(); 88 | const ints = [ 2, 6, 9]; 89 | enc ~= ints; 90 | auto dec = Decerealiser(enc.bytes); 91 | dec.value!(int[]).shouldEqual(ints); 92 | dec.value!ubyte.shouldThrow!RangeError; //no more bytes 93 | } 94 | 95 | 96 | @("struct with @LengthType") unittest { 97 | import cerealed.attrs: LengthType; 98 | struct Foo { 99 | @LengthType!ubyte ushort[] arr; 100 | } 101 | 102 | auto enc = Cerealiser(); 103 | auto foo = Foo([7, 8, 9]); 104 | enc ~= foo; 105 | enc.bytes.shouldEqual([3, 0, 7, 0, 8, 0, 9]); 106 | auto dec = Decerealiser(enc.bytes); 107 | dec.value!Foo.shouldEqual(foo); 108 | } 109 | 110 | void testEncDecAssocArray() { 111 | auto enc = Cerealiser(); 112 | const intToInts = [ 1:2, 3:6, 9:18]; 113 | enc ~= intToInts; 114 | auto dec = Decerealiser(enc.bytes); 115 | dec.value!(int[int]).shouldEqual(intToInts); 116 | dec.value!ubyte.shouldThrow!RangeError; //no more bytes 117 | } 118 | -------------------------------------------------------------------------------- /tests/enums.d: -------------------------------------------------------------------------------- 1 | module tests.enums; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | import core.exception; 6 | 7 | 8 | private enum MyEnum { Foo, Bar, Baz }; 9 | 10 | void testEnum() { 11 | auto enc = Cerealiser(); 12 | enc ~= MyEnum.Bar; 13 | enc ~= MyEnum.Baz; 14 | enc ~= MyEnum.Foo; 15 | enc.bytes.shouldEqual([0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0]); 16 | 17 | auto dec = Decerealizer(enc.bytes); 18 | dec.value!MyEnum.shouldEqual(MyEnum.Bar); 19 | dec.value!MyEnum.shouldEqual(MyEnum.Baz); 20 | dec.value!MyEnum.shouldEqual(MyEnum.Foo); 21 | dec.value!ubyte.shouldThrow!RangeError; 22 | } 23 | 24 | void testDecodeEnum() { 25 | enum Foo : ubyte { 26 | Bar = 0, 27 | Baz = 1 28 | } 29 | 30 | auto cereal = Decerealiser([ 0, 1 ]); 31 | shouldEqual(cereal.value!Foo, Foo.Bar); 32 | shouldEqual(cereal.value!Foo, Foo.Baz); 33 | cereal.value!ubyte.shouldThrow!RangeError; 34 | } 35 | -------------------------------------------------------------------------------- /tests/example.d: -------------------------------------------------------------------------------- 1 | module tests.example; 2 | 3 | @("example") 4 | unittest { 5 | import cerealed; 6 | assert(cerealise(5) == [0, 0, 0, 5]); // returns ubyte[] 7 | cerealise!(a => assert(a == [0, 0, 0, 5]))(5); // faster than using the bytes directly 8 | 9 | assert(decerealise!int([0, 0, 0, 5]) == 5); 10 | 11 | struct Foo { int i; } 12 | const foo = Foo(5); 13 | // alternate spelling 14 | assert(foo.cerealize.decerealize!Foo == foo); 15 | } 16 | -------------------------------------------------------------------------------- /tests/multidimensional_array.d: -------------------------------------------------------------------------------- 1 | module tests.multidimensional_array; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | 6 | 7 | void testEmptyMultidimensionalArray() { 8 | int[][] original, restored; 9 | auto enc = Cerealiser(); 10 | enc ~= original; 11 | auto dec = Decerealiser(enc.bytes); 12 | restored = dec.value!(int[][]); 13 | assert(original == restored); 14 | } 15 | 16 | 17 | void testMultidimensionalArray() { 18 | int[][] some = [ 19 | [3, 5, 6], 20 | [-3, 6, int.max, int.min], 21 | ]; 22 | auto enc = Cerealiser(); 23 | enc ~= some; 24 | writelnUt(enc.bytes); 25 | enc.bytes.shouldEqual([ 26 | 0, 2, 27 | 0, 3, 28 | 0, 0, 0, 3, 29 | 0, 0, 0, 5, 30 | 0, 0, 0, 6, 31 | 0, 4, 32 | 255, 255, 255, 253, 33 | 0, 0, 0, 6, 34 | 127, 255, 255, 255, 35 | 128, 0, 0, 0 36 | ]); 37 | auto dec = Decerealiser(enc.bytes); 38 | assert(some == dec.value!(int[][])); 39 | } 40 | -------------------------------------------------------------------------------- /tests/nested.d: -------------------------------------------------------------------------------- 1 | module tests.nested; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | 6 | struct Nested { 7 | Nested[int] aa; 8 | } 9 | 10 | struct SomeStruct { 11 | string[] str; 12 | int[][] ints; 13 | Nested[] nesteds; 14 | } 15 | 16 | void testEmptyNested() { 17 | SomeStruct original, restored; 18 | auto enc = Cerealiser(); 19 | enc ~= original; 20 | auto dec = Decerealiser(enc.bytes); 21 | restored = dec.value!(SomeStruct); 22 | 23 | original.shouldEqual(restored); 24 | } 25 | 26 | 27 | void testNested() { 28 | auto some = SomeStruct(["foo", "sunny"], 29 | [[2, 4], [1, 3, 5]], 30 | [Nested([7: Nested()])]); 31 | auto enc = Cerealiser(); 32 | enc ~= some; 33 | enc.bytes.shouldEqual([0, 2, 0, 3, 'f', 'o', 'o', 0, 5, 's', 'u', 'n', 'n', 'y', 34 | 0, 2, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4, 0, 3, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 35 | 0, 1, 0, 1, 0, 0, 0, 7, 0, 0]); 36 | auto dec = Decerealiser(enc.bytes); 37 | dec.value!SomeStruct.shouldEqual(some); 38 | } 39 | 40 | void testNestedDynamic() { 41 | SomeStruct[] some; 42 | some ~= SomeStruct(["foo", "sunny"], 43 | [[2, 4], [1, 3, 5]], 44 | [Nested([7: Nested()])]); 45 | auto enc = Cerealiser(); 46 | enc ~= some; 47 | enc.bytes.shouldEqual([0, 1, 0, 2, 0, 3, 'f', 'o', 'o', 0, 5, 's', 'u', 'n', 'n', 'y', 48 | 0, 2, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4, 0, 3, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 49 | 0, 1, 0, 1, 0, 0, 0, 7, 0, 0]); 50 | auto dec = Decerealiser(enc.bytes); 51 | dec.value!(SomeStruct[]).shouldEqual(some); 52 | } 53 | -------------------------------------------------------------------------------- /tests/pointers.d: -------------------------------------------------------------------------------- 1 | module tests.pointers; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | import core.exception; 6 | 7 | 8 | private struct InnerStruct { 9 | ubyte b; 10 | ushort s; 11 | } 12 | 13 | private struct OuterStructWithPointerToStruct { 14 | ushort s; 15 | InnerStruct* inner; 16 | ubyte b; 17 | } 18 | 19 | void testStructWithPointerToStruct() { 20 | auto enc = Cerealiser(); 21 | //outer not const because not copyable from const 22 | auto outer = OuterStructWithPointerToStruct(3, new InnerStruct(7, 2), 5); 23 | enc ~= outer; 24 | 25 | const bytes = [ 0, 3, 7, 0, 2, 5]; 26 | enc.bytes.shouldEqual(bytes); 27 | 28 | auto dec = Decerealiser(bytes); 29 | const decOuter = dec.value!OuterStructWithPointerToStruct; 30 | 31 | //can't compare the two structs directly since the pointers 32 | //won't match but the values will. 33 | decOuter.s.shouldEqual(outer.s); 34 | shouldEqual(*decOuter.inner, *outer.inner); 35 | decOuter.inner.shouldNotEqual(outer.inner); //ptrs shouldn't match 36 | decOuter.b.shouldEqual(outer.b); 37 | 38 | dec.value!ubyte.shouldThrow!RangeError; //no bytes 39 | } 40 | 41 | 42 | private class InnerClass { 43 | ushort s; 44 | ubyte b; 45 | this() {} //needed for decerealisation 46 | this(ushort s, ubyte b) { this.s = s; this.b = b; } 47 | override string toString() const { //so it can be used in shouldEqual 48 | import std.conv; 49 | return text("InnerClass(", s, ", ", b, ")"); 50 | } 51 | 52 | } 53 | 54 | private struct OuterStructWithClass { 55 | ushort s; 56 | InnerClass inner; 57 | ubyte b; 58 | } 59 | 60 | void testStructWithClassReference() { 61 | auto enc = Cerealiser(); 62 | auto outer = OuterStructWithClass(2, new InnerClass(3, 5), 8); 63 | enc ~= outer; 64 | 65 | const bytes = [ 0, 2, 0, 3, 5, 8]; 66 | enc.bytes.shouldEqual(bytes); 67 | 68 | auto dec = Decerealiser(bytes); 69 | const decOuter = dec.value!OuterStructWithClass; 70 | 71 | //can't compare the two structs directly since the pointers 72 | //won't match but the values will. 73 | decOuter.s.shouldEqual(outer.s); 74 | decOuter.inner.shouldEqual(outer.inner); 75 | shouldNotEqual(&decOuter.inner, &outer.inner); //ptrs shouldn't match 76 | decOuter.b.shouldEqual(outer.b); 77 | } 78 | 79 | void testPointerToInt() { 80 | auto enc = Cerealiser(); 81 | auto i = new int; *i = 4; 82 | enc ~= i; 83 | const bytes = [0, 0, 0, 4]; 84 | enc.bytes.shouldEqual(bytes); 85 | 86 | auto dec = Decerealiser(bytes); 87 | shouldEqual(*dec.value!(int*), *i); 88 | } 89 | -------------------------------------------------------------------------------- /tests/property.d: -------------------------------------------------------------------------------- 1 | module tests.property; 2 | 3 | import cerealed; 4 | import unit_threaded; 5 | import std.meta: AliasSeq; 6 | 7 | 8 | static foreach(T; AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, 9 | float, double, 10 | char, wchar, dchar, 11 | ubyte[], ushort[], int[], long[], float[], double[])) 12 | unittest { 13 | check!((T val) => val.cerealise.decerealise!T == val); 14 | } 15 | 16 | 17 | @("array with non-default length type") 18 | @safe unittest { 19 | check!((ubyte[] arr) { 20 | if(arr.length > ubyte.max) { 21 | return true; 22 | } 23 | auto enc = Cerealiser(); 24 | enc.grain!ubyte(arr); 25 | enc.bytes.length.shouldEqual(arr.length + ubyte.sizeof); 26 | auto dec = Decerealiser(enc.bytes); 27 | ubyte[] arr2; 28 | dec.grain!ubyte(arr2); 29 | return arr2 == arr; 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /tests/protocol_unit.d: -------------------------------------------------------------------------------- 1 | module tests.protocol_unit; 2 | 3 | 4 | import cerealed; 5 | import unit_threaded; 6 | import core.exception; 7 | 8 | 9 | struct Unit { 10 | ushort us; 11 | ubyte ub1; 12 | ubyte ub2; 13 | } 14 | 15 | struct Packet { 16 | ubyte ub1; 17 | ushort length; 18 | ubyte ub2; 19 | @ArrayLength("length") Unit[] units; 20 | } 21 | 22 | 23 | void testUnits() { 24 | ubyte[] bytes = [3, 0, 4, 9, 0, 7, 1, 2, 0, 6, 2, 3, 0, 5, 4, 5, 0, 4, 9, 8]; 25 | auto pkt = decerealise!Packet(bytes); 26 | 27 | pkt.ub1.shouldEqual(3); 28 | pkt.length.shouldEqual(4); 29 | pkt.ub2.shouldEqual(9); 30 | pkt.units.length.shouldEqual(pkt.length); 31 | 32 | pkt.units[1].us.shouldEqual(6); 33 | pkt.units[1].ub1.shouldEqual(2); 34 | pkt.units[1].ub2.shouldEqual(3); 35 | 36 | auto enc = Cerealiser(); 37 | enc ~= pkt; 38 | enc.bytes.shouldEqual(bytes); 39 | } 40 | 41 | 42 | struct PacketWithArrayLengthExpr { 43 | static struct Header { 44 | ubyte ub; 45 | ushort us; 46 | ushort length; 47 | } 48 | 49 | enum headerSize = unalignedSizeof!Header; 50 | alias header this; 51 | 52 | Header header; 53 | @ArrayLength("length - headerSize") Unit[] units; 54 | } 55 | 56 | void testArrayLengthExpr() { 57 | immutable ubyte[] bytes = [3, 0, 7, 0, 8, //header 58 | 0, 7, 1, 2, 59 | 0, 6, 2, 3, 60 | 0, 5, 4, 5]; 61 | auto pkt = decerealise!PacketWithArrayLengthExpr(bytes); 62 | 63 | pkt.ub.shouldEqual(3); 64 | pkt.us.shouldEqual(7); 65 | pkt.units.length.shouldEqual(3); 66 | 67 | pkt.units[2].us.shouldEqual(5); 68 | pkt.units[2].ub1.shouldEqual(4); 69 | pkt.units[2].ub2.shouldEqual(5); 70 | 71 | auto enc = Cerealiser(); 72 | enc ~= pkt; 73 | enc.bytes.shouldEqual(bytes); 74 | } 75 | 76 | 77 | struct NegativeStruct { 78 | enum len = -1; 79 | ushort us; 80 | @ArrayLength("len") Unit[] units; 81 | } 82 | 83 | void testNegativeLength() { 84 | immutable ubyte[] bytes = [1, 2, 3, 4, 5]; 85 | decerealise!NegativeStruct(bytes).shouldThrow!CerealException; 86 | } 87 | 88 | 89 | void testNotEnoughBytes() { 90 | immutable ubyte[] bytes = [3, 0, 7, 0, 8, //header 91 | 0, 7]; //truncated 92 | decerealise!PacketWithArrayLengthExpr(bytes).shouldThrow!CerealException; 93 | } 94 | 95 | 96 | struct PacketWithLengthInBytes { 97 | static struct Header { 98 | ubyte ub; 99 | ushort lengthNoHeader; 100 | } 101 | 102 | enum headerSize = unalignedSizeof!Header; 103 | alias header this; 104 | 105 | Header header; 106 | @LengthInBytes("lengthNoHeader - headerSize") Unit[] units; 107 | } 108 | 109 | void testLengthInBytes() { 110 | immutable ubyte[] bytes = [ 7, 0, 11, //header (11 bytes = hdr + 2 units of 4 bytes each) 111 | 0, 3, 1, 2, 112 | 0, 9, 3, 4, 113 | ]; 114 | auto pkt = decerealise!PacketWithLengthInBytes(bytes); 115 | 116 | pkt.ub.shouldEqual(7); 117 | pkt.lengthNoHeader.shouldEqual(11); 118 | pkt.units.length.shouldEqual(2); 119 | 120 | pkt.units[0].us.shouldEqual(3); 121 | pkt.units[0].ub1.shouldEqual(1); 122 | pkt.units[0].ub2.shouldEqual(2); 123 | 124 | pkt.units[1].us.shouldEqual(9); 125 | pkt.units[1].ub1.shouldEqual(3); 126 | pkt.units[1].ub2.shouldEqual(4); 127 | 128 | auto enc = Cerealiser(); 129 | enc ~= pkt; 130 | enc.bytes.shouldEqual(bytes); 131 | } 132 | 133 | 134 | struct BigUnit { 135 | int i1; 136 | int i2; 137 | } 138 | 139 | struct BigUnitPacket { 140 | enum headerSize = totalLength.sizeof; 141 | ushort totalLength; 142 | @LengthInBytes("totalLength - headerSize") BigUnit[] units; 143 | } 144 | 145 | void testLengthInBytesOneUnit() { 146 | immutable ubyte[] bytes = [ 0, 10, //totalLength = 1 unit of size 8 + header size of 1 147 | 0, 0, 0, 1, 0, 0, 0, 2 148 | ]; 149 | auto pkt = decerealise!BigUnitPacket(bytes); 150 | 151 | pkt.totalLength.shouldEqual(bytes.length); 152 | pkt.units.length.shouldEqual(1); 153 | pkt.units[0].i1.shouldEqual(1); 154 | pkt.units[0].i2.shouldEqual(2); 155 | 156 | auto enc = Cerealiser(); 157 | enc ~= pkt; 158 | enc.bytes.shouldEqual(bytes); 159 | } 160 | 161 | 162 | @("RestOfPacket with LengthInBytes") 163 | unittest { 164 | struct Small { 165 | ushort v; 166 | } 167 | struct Unit { 168 | ushort length; 169 | @LengthInBytes("length - 2") Small[] smalls; 170 | } 171 | struct Struct { 172 | @RestOfPacket Unit[] units; 173 | } 174 | immutable ubyte[] bytes = 175 | [ 176 | 0, 6, //length 177 | 0, 1, 0, 2, // smalls 178 | 0, 4, //length 179 | 0, 3, //smalls 180 | ]; 181 | 182 | auto dec = Decerealiser(bytes); 183 | auto pkt = dec.value!Struct; 184 | 185 | pkt.shouldEqual( 186 | Struct([Unit(6, 187 | [ 188 | Small(1), 189 | Small(2), 190 | ] 191 | ), 192 | Unit(4, 193 | [ 194 | Small(3), 195 | ] 196 | )])); 197 | } 198 | -------------------------------------------------------------------------------- /tests/range.d: -------------------------------------------------------------------------------- 1 | module tests.range; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | import std.range; 6 | import core.exception; 7 | 8 | 9 | void testInputRange() { 10 | auto enc = Cerealiser(); 11 | enc ~= iota(cast(ubyte)5); 12 | enc.bytes.shouldEqual([0, 5, 0, 1, 2, 3, 4]); 13 | } 14 | 15 | private ubyte[] gOutputBytes; 16 | 17 | private struct MyOutputRange { 18 | void put(in ubyte b) { 19 | gOutputBytes ~= b; 20 | } 21 | } 22 | 23 | void testMyOutputRange() { 24 | static assert(isOutputRange!(MyOutputRange, ubyte)); 25 | } 26 | 27 | @SingleThreaded 28 | void testOutputRangeValue() { 29 | gOutputBytes = []; 30 | 31 | auto dec = Decerealiser([0, 5, 2, 3, 9, 6, 1]); 32 | auto output = dec.value!MyOutputRange; 33 | 34 | gOutputBytes.shouldEqual([2, 3, 9, 6, 1]); 35 | } 36 | 37 | @SingleThreaded 38 | void testOutputRangeRead() { 39 | gOutputBytes = []; 40 | 41 | auto dec = Decerealiser([0, 5, 2, 3, 9, 6, 1]); 42 | auto output = MyOutputRange(); 43 | dec.read(output); 44 | 45 | gOutputBytes.shouldEqual([2, 3, 9, 6, 1]); 46 | } 47 | 48 | 49 | private ubyte[] gInputBytes; 50 | 51 | private struct MyInputRange { 52 | this(ubyte[] bytes) { gInputBytes = bytes; } 53 | ubyte front() { return gInputBytes.front; } 54 | void popFront() { gInputBytes.popFront; } 55 | bool empty() { return gInputBytes.empty(); } 56 | @property ulong length() { return gInputBytes.length; } 57 | static assert(isInputRange!MyInputRange); 58 | } 59 | 60 | private struct StructWithInputRange { 61 | ubyte b; 62 | MyInputRange input; 63 | } 64 | 65 | @SingleThreaded 66 | void testEmbeddedInputRange() { 67 | auto enc = Cerealiser(); 68 | auto str = StructWithInputRange(2, MyInputRange([9, 7, 6])); 69 | enc ~= str; 70 | const bytes = [2, 0, 3, 9, 7, 6]; 71 | enc.bytes.shouldEqual(bytes); 72 | 73 | //no deserialisation for InputRange 74 | auto dec = Decerealiser(bytes); 75 | enum compiles = __traits(compiles, { dec.value!StructWithInputRange; }); 76 | static assert(!compiles, "Should not be able to read into an InputRange"); 77 | } 78 | 79 | private struct StructWithOutputRange { 80 | ubyte b1; 81 | MyOutputRange output; 82 | ubyte b2; 83 | } 84 | 85 | @SingleThreaded 86 | void testEmbeddedOutputRange() { 87 | auto enc = Cerealiser(); 88 | enum compiles = __traits(compiles, { enc ~= StructWithOutputRange(); }); 89 | static assert(!compiles, "Should not be able to read from an OutputRange"); 90 | gOutputBytes = []; 91 | auto dec = Decerealiser([255, //1st byte 92 | 0, 3, 9, 7, 6, //length, values 93 | 12]); //2nd byte 94 | dec.bytes.shouldEqual([255, 0, 3, 9, 7, 6, 12]); 95 | const str = dec.value!StructWithOutputRange; 96 | writelnUt("dec bytes is ", dec.bytes); 97 | dec.value!ubyte.shouldThrow!RangeError; //no more bytes 98 | 99 | str.b1.shouldEqual(255); 100 | str.b2.shouldEqual(12); 101 | gOutputBytes.shouldEqual([9, 7, 6]); 102 | } 103 | -------------------------------------------------------------------------------- /tests/reset.d: -------------------------------------------------------------------------------- 1 | module tests.reset; 2 | 3 | import unit_threaded; 4 | import cerealed.cerealiser; 5 | import cerealed.decerealiser; 6 | 7 | 8 | void testResetCerealiser() { 9 | auto enc = Cerealiser(); 10 | enc ~= 5; 11 | enc ~= 'a'; 12 | enc.bytes.shouldEqual([0, 0, 0, 5, 'a']); 13 | const bytesSlice = enc.bytes; 14 | 15 | enc.reset(); 16 | 17 | enc.bytes.shouldBeEmpty; 18 | bytesSlice.shouldEqual([0, 0, 0, 5, 'a']); 19 | 20 | enc ~= 2; 21 | enc.bytes.shouldEqual([0, 0, 0, 2]); 22 | bytesSlice.shouldEqual([0, 0, 0, 2, 'a']); 23 | } 24 | 25 | 26 | void testResetDecerealiser() { 27 | const ubyte[] bytes1 = [1, 2, 3, 5, 8, 13]; 28 | auto dec = Decerealiser(bytes1); 29 | 30 | dec.value!int; //get rid of 4 bytes 31 | dec.bytes.shouldEqual([8, 13]); 32 | 33 | dec.value!short; //get rid of the remaining 2 bytes 34 | dec.bytes.shouldBeEmpty; 35 | 36 | dec.reset(); 37 | dec.bytes.shouldEqual(bytes1); 38 | 39 | const ubyte[] bytes2 = [3, 6, 9, 12]; 40 | dec.reset(bytes2); 41 | dec.bytes.shouldEqual(bytes2); 42 | } 43 | 44 | 45 | void testEmptyDecerealiser() { 46 | import core.exception: RangeError; 47 | auto dec = Decerealiser(); 48 | dec.value!ubyte.shouldThrow!RangeError; //no bytes 49 | } 50 | -------------------------------------------------------------------------------- /tests/static_array.d: -------------------------------------------------------------------------------- 1 | module tests.static_array; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | 6 | void testStaticArray() { 7 | int[2] original, restored; 8 | 9 | original[0] = 34; 10 | original[1] = 45; 11 | auto enc = Cerealiser(); 12 | enc ~= original; 13 | assert(enc.bytes == [ 14 | // no length because it's already known 15 | 0, 0, 0, 34, 16 | 0, 0, 0, 45, 17 | ]); 18 | auto dec = Decerealiser(enc.bytes); 19 | restored = dec.value!(int[2]); 20 | 21 | assert(original == restored); 22 | } 23 | 24 | 25 | void testArrayOfArrays() { 26 | static struct Unit { 27 | ubyte thingie; 28 | ushort length; 29 | @NoCereal ubyte[] data; 30 | 31 | void postBlit(C)(auto ref C cereal) if(isCereal!C) { 32 | static if(isDecerealiser!C) { 33 | writelnUt("Decerealiser bytesLeft ", cereal.bytesLeft); 34 | writelnUt("length: ", length); 35 | } 36 | cereal.grainLengthedArray(data, length); 37 | } 38 | } 39 | 40 | static struct Packet { 41 | ubyte vrsion; 42 | @RawArray Unit[] units; 43 | } 44 | 45 | ubyte[] bytes = [7, //vrsion 46 | 1, 0, 3, 0xa, 0xb, 0xc, //1st unit 47 | 2, 0, 5, 1, 2, 3, 4, 5 //2nd unit 48 | ]; 49 | 50 | auto dec = Decerealiser(bytes); 51 | auto pkt = dec.value!Packet; 52 | pkt.shouldEqual(Packet(7, [Unit(1, 3, [0xa, 0xb, 0xc]), Unit(2, 5, [1, 2, 3, 4, 5])])); 53 | 54 | auto enc = Cerealiser(); 55 | enc ~= pkt; 56 | enc.bytes.shouldEqual(bytes); 57 | } 58 | -------------------------------------------------------------------------------- /tests/structs.d: -------------------------------------------------------------------------------- 1 | module tests.structs; 2 | 3 | import unit_threaded; 4 | import cerealed; 5 | import std.conv; 6 | import core.exception; 7 | 8 | 9 | private struct DummyStruct { 10 | int i; 11 | double d; 12 | int[] a; 13 | bool b; 14 | double[int] aa; 15 | string s; 16 | 17 | void foo() {} 18 | } 19 | 20 | 21 | void testDummyStruct() { 22 | auto enc = Cerealiser(); 23 | auto dummy = DummyStruct(5, 6.0, [2, 3], true, [2: 4.0], "dummy!"); 24 | enc ~= dummy; 25 | 26 | auto dec = Decerealiser(enc.bytes); 27 | dec.value!DummyStruct.shouldEqual(dummy); 28 | 29 | dec.value!ubyte.shouldThrow!RangeError; 30 | } 31 | 32 | private struct StringStruct { 33 | string s; 34 | } 35 | 36 | void testDecodeStringStruct() { 37 | auto dec = Decerealiser([0, 3, 'f', 'o', 'o']); 38 | auto str = StringStruct(); 39 | dec.grain(str); 40 | str.s.shouldEqual("foo"); 41 | dec.value!ubyte.shouldThrow!RangeError; 42 | } 43 | 44 | void testEncodeStringStruct() { 45 | auto enc = Cerealiser(); 46 | const str = StringStruct("foo"); 47 | enc ~= str; 48 | enc.bytes.shouldEqual([ 0, 3, 'f', 'o', 'o']); 49 | } 50 | 51 | 52 | private struct ProtoHeaderStruct { 53 | @Bits!3 ubyte bits3; 54 | @Bits!1 ubyte bits1; 55 | @Bits!4 uint bits4; 56 | ubyte bits8; //no UDA necessary 57 | } 58 | 59 | 60 | void testEncDecProtoHeaderStruct() { 61 | const hdr = ProtoHeaderStruct(6, 1, 3, 254); 62 | auto enc = Cerealiser(); 63 | enc ~= hdr; //1101 0011, 254 64 | enc.bytes.shouldEqual([0xd3, 254]); 65 | 66 | auto dec = Decerealiser(enc.bytes); 67 | dec.value!ProtoHeaderStruct.shouldEqual(hdr); 68 | } 69 | 70 | private struct StructWithNoCereal { 71 | @Bits!4 ubyte nibble1; 72 | @Bits!4 ubyte nibble2; 73 | @NoCereal ushort nocereal1; 74 | ushort value; 75 | @NoCereal ushort nocereal2; 76 | } 77 | 78 | void testNoCereal() { 79 | auto cerealizer = Cerealizer(); 80 | cerealizer ~= StructWithNoCereal(3, 14, 42, 5, 12); 81 | //only nibble1, nibble2 and value should show up in bytes 82 | immutable bytes = [0x3e, 0x00, 0x05]; 83 | cerealizer.bytes.shouldEqual(bytes); 84 | 85 | auto decerealizer = Decerealizer(bytes); 86 | //won't be the same as the serialised struct, since the members 87 | //marked with NoCereal will be set to T.init 88 | decerealizer.value!StructWithNoCereal.shouldEqual(StructWithNoCereal(3, 14, 0, 5, 0)); 89 | } 90 | 91 | private struct CustomStruct { 92 | ubyte mybyte; 93 | ushort myshort; 94 | void accept(Cereal)(ref Cereal cereal) { 95 | //can't call grain(this), that would cause an infinite loop 96 | cereal.grainAllMembers(this); 97 | ubyte otherbyte = 4; 98 | cereal.grain(otherbyte); 99 | } 100 | } 101 | 102 | void testCustomCereal() { 103 | auto cerealiser = Cerealiser(); 104 | cerealiser ~= CustomStruct(1, 2); 105 | cerealiser.bytes.shouldEqual([ 1, 0, 2, 4]); 106 | 107 | //because of the custom serialisation, passing in just [1, 0, 2] would throw 108 | auto decerealiser = Decerealiser([1, 0, 2, 4]); 109 | decerealiser.value!CustomStruct.shouldEqual(CustomStruct(1, 2)); 110 | } 111 | 112 | 113 | void testAttrMember() { 114 | //test that attributes work when calling grain member by member 115 | auto cereal = Cerealizer(); 116 | auto str = StructWithNoCereal(3, 14, 42, 5, 12); 117 | cereal.grainMemberWithAttr!"nibble1"(str); 118 | cereal.grainMemberWithAttr!"nibble2"(str); 119 | cereal.grainMemberWithAttr!"nocereal1"(str); 120 | cereal.grainMemberWithAttr!"value"(str); 121 | cereal.grainMemberWithAttr!"nocereal2"(str); 122 | 123 | //only nibble1, nibble2 and value should show up in bytes 124 | cereal.bytes.shouldEqual([0x3e, 0x00, 0x05]); 125 | } 126 | 127 | struct EnumStruct { 128 | enum Enum:byte { 129 | Foo, 130 | Bar, 131 | Baz 132 | } 133 | 134 | ubyte foo; 135 | Enum bar; 136 | } 137 | 138 | void testEnum() { 139 | auto cerealiser = Cerealiser(); 140 | const e = EnumStruct(1, EnumStruct.Enum.Baz); 141 | cerealiser ~= e; 142 | const bytes = [1, 2]; 143 | cerealiser.bytes.shouldEqual(bytes); 144 | 145 | auto decerealiser = Decerealiser(bytes); 146 | decerealiser.value!EnumStruct.shouldEqual(e); 147 | } 148 | 149 | struct PostBlitStruct { 150 | ubyte foo; 151 | @NoCereal ubyte bar; 152 | ubyte baz; 153 | void postBlit(Cereal)(ref Cereal cereal) { 154 | ushort foo = 4; 155 | cereal.grain(foo); 156 | } 157 | } 158 | 159 | void testPostBlit() { 160 | auto enc = Cerealiser(); 161 | enc ~= PostBlitStruct(3, 5, 8); 162 | const bytes = [ 3, 8, 0, 4]; 163 | enc.bytes.shouldEqual(bytes); 164 | 165 | auto dec = Decerealiser(bytes); 166 | dec.value!PostBlitStruct.shouldEqual(PostBlitStruct(3, 0, 8)); 167 | } 168 | 169 | private struct StringsStruct { 170 | ubyte mybyte; 171 | @RawArray string[] strings; 172 | } 173 | 174 | void testRawArray() { 175 | auto enc = Cerealiser(); 176 | auto strs = StringsStruct(5, ["foo", "foobar", "ohwell"]); 177 | enc ~= strs; 178 | //no length encoding for the array, but strings still get a length each 179 | const bytes = [ 5, 0, 3, 'f', 'o', 'o', 0, 6, 'f', 'o', 'o', 'b', 'a', 'r', 180 | 0, 6, 'o', 'h', 'w', 'e', 'l', 'l']; 181 | enc.bytes.shouldEqual(bytes); 182 | 183 | auto dec = Decerealiser(bytes); 184 | dec.value!StringsStruct.shouldEqual(strs); 185 | } 186 | 187 | void testReadmeCode() { 188 | struct MyStruct { 189 | ubyte mybyte1; 190 | @NoCereal uint nocereal1; //won't be serialised 191 | //the next 3 members will all take up one byte 192 | @Bits!4 ubyte nibble; //gets packed into 4 bits 193 | @Bits!1 ubyte bit; //gets packed into 1 bit 194 | @Bits!3 ubyte bits3; //gets packed into 3 bits 195 | ubyte mybyte2; 196 | } 197 | 198 | auto enc = Cerealiser(); 199 | enc ~= MyStruct(3, 123, 14, 1, 2, 42); 200 | import std.conv; 201 | assert(enc.bytes == [ 3, 0xea /*1110 1 010*/, 42], text("bytes were ", enc.bytes)); 202 | 203 | auto dec = Decerealizer([ 3, 0xea, 42]); //US spelling works too 204 | //the 2nd value is 0 and not 123 since that value 205 | //doesn't get serialised/deserialised 206 | auto val = dec.value!MyStruct; 207 | assert(val == MyStruct(3, 0, 14, 1, 2, 42), text("struct was ", val)); 208 | } 209 | 210 | 211 | private enum MqttType { 212 | RESERVED1 = 0, CONNECT = 1, CONNACK = 2, PUBLISH = 3, 213 | PUBACK = 4, PUBREC = 5, PUBREL = 6, PUBCOMP = 7, 214 | SUBSCRIBE = 8, SUBACK = 9, UNSUBSCRIBE = 10, UNSUBACK = 11, 215 | PINGREQ = 12, PINGRESP = 13, DISCONNECT = 14, RESERVED2 = 15 216 | } 217 | 218 | private struct MqttFixedHeader { 219 | public: 220 | enum SIZE = 2; 221 | 222 | @Bits!4 MqttType type; 223 | @Bits!1 bool dup; 224 | @Bits!2 ubyte qos; 225 | @Bits!1 bool retain; 226 | @NoCereal uint remaining; 227 | 228 | void postBlit(Cereal)(ref Cereal cereal) if(isCerealiser!Cereal) { 229 | setRemainingSize(cereal); 230 | } 231 | 232 | void postBlit(Cereal)(ref Cereal cereal) if(isDecerealiser!Cereal) { 233 | remaining = getRemainingSize(cereal); 234 | } 235 | 236 | private: 237 | 238 | uint getRemainingSize(Cereal)(ref Cereal cereal) { 239 | //algorithm straight from the MQTT spec 240 | int multiplier = 1; 241 | uint value = 0; 242 | ubyte digit; 243 | do { 244 | cereal.grain(digit); 245 | value += (digit & 127) * multiplier; 246 | multiplier *= 128; 247 | } while((digit & 128) != 0); 248 | 249 | return value; 250 | } 251 | 252 | void setRemainingSize(Cereal)(ref Cereal cereal) const { 253 | //algorithm straight from the MQTT spec 254 | ubyte[] digits; 255 | uint x = remaining; 256 | do { 257 | ubyte digit = x % 128; 258 | x /= 128; 259 | if(x > 0) { 260 | digit = digit | 0x80; 261 | } 262 | digits ~= digit; 263 | } while(x > 0); 264 | 265 | foreach(b; digits) cereal.grain(b); 266 | } 267 | } 268 | 269 | void testAcceptPostBlitAttrs() { 270 | import cerealed.traits; 271 | static assert(hasPostBlit!MqttFixedHeader); 272 | static assert(hasAccept!CustomStruct); 273 | mixin assertHasPostBlit!MqttFixedHeader; 274 | mixin assertHasAccept!CustomStruct; 275 | 276 | } 277 | 278 | void testCerealiseMqttHeader() { 279 | auto cereal = Cerealiser(); 280 | cereal ~= MqttFixedHeader(MqttType.PUBLISH, true, 2, false, 5); 281 | cereal.bytes.shouldEqual([0x3c, 0x5]); 282 | } 283 | 284 | void testDecerealiseMqttHeader() { 285 | auto cereal = Decerealiser([0x3c, 0x5]); 286 | cereal.value!MqttFixedHeader.shouldEqual(MqttFixedHeader(MqttType.PUBLISH, true, 2, false, 5)); 287 | } 288 | 289 | 290 | class CustomException: Exception { 291 | this(string msg) { 292 | super(msg); 293 | } 294 | } 295 | 296 | struct StructWithPreBlit { 297 | static struct Header { 298 | uint i; 299 | } 300 | 301 | alias header this; 302 | enum headerSize = unalignedSizeof!Header; 303 | 304 | Header header; 305 | ubyte ub1; 306 | ubyte ub2; 307 | 308 | void preBlit(C)(auto ref C cereal) { 309 | static if(isDecerealiser!C) { 310 | if(cereal.bytesLeft < headerSize) 311 | throw new CustomException( 312 | text("Cannot decerealise into header of size ", headerSize, 313 | " when there are only ", cereal.bytesLeft, " bytes left")); 314 | } 315 | } 316 | 317 | mixin assertHasPreBlit!StructWithPreBlit; 318 | } 319 | 320 | void testPreBlit() { 321 | immutable ubyte[] bytesOk = [0, 0, 0, 3, 1, 2]; 322 | bytesOk.decerealise!StructWithPreBlit; 323 | 324 | immutable ubyte[] bytesOops = [0, 0, 0]; 325 | bytesOops.decerealise!StructWithPreBlit.shouldThrow!CustomException; 326 | 327 | // otherwise fails to throw RangeError with --build=release 328 | debug { 329 | immutable ubyte[] bytesMegaOops = [0, 0, 0, 3]; 330 | bytesMegaOops.decerealise!StructWithPreBlit.shouldThrow!RangeError; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /tests/utils.d: -------------------------------------------------------------------------------- 1 | module tests.utils; 2 | 3 | 4 | import cerealed; 5 | import unit_threaded; 6 | 7 | 8 | struct SimpleStruct { 9 | ubyte ub; 10 | ushort us1; 11 | ushort us2; 12 | } 13 | 14 | void testSizeofSimpleStruct() { 15 | unalignedSizeof!SimpleStruct.shouldEqual(5); 16 | } 17 | 18 | 19 | struct Outer { 20 | SimpleStruct inner; 21 | } 22 | 23 | void testSizeOfStructWithStructs() { 24 | unalignedSizeof!Outer.shouldEqual(5); 25 | } 26 | 27 | 28 | union Union { 29 | ubyte ub; 30 | ushort us; 31 | } 32 | 33 | void testSizeOfUnion() { 34 | unalignedSizeof!Union.shouldEqual(2); 35 | } 36 | 37 | 38 | class Class { 39 | ubyte ub; 40 | ushort us; 41 | } 42 | 43 | void testSizeOfClass() { 44 | unalignedSizeof!Class.shouldEqual(3); 45 | } 46 | --------------------------------------------------------------------------------