1 /// A serializer and pull parser for a custom binary format. The format is meant to be simple, so it might lack
2 /// features. It does not store field names or type information.
3 module rcdata.bin;
4 
5 import std.range;
6 import std.traits;
7 import std.bitmanip;
8 
9 // Implementation notes:
10 // * Use `ulong` instead of `size_t` to keep the data consistent between devices.
11 // * Numbers should be stored in little endian using std.bitmanip.nativeToLittleEndian.
12 
13 /// Make an rcbin parser.
14 RCBin!(T, true) rcbinParser(T)(ref T range) {
15 
16     return RCBin!(T, true)(range);
17 
18 }
19 
20 /// Make an rcbin serializer.
21 RCBin!(T, false) rcbinSerializer(T)(ref T range) {
22 
23     return RCBin!(T, false)(range);
24 
25 }
26 
27 /// Check if the given type is an RCBin struct.
28 enum isRCBin(T) = is(T == RCBin!(A, B), A, alias B);
29 
30 /// Struct for parsing and serializing into binary.
31 /// Params:
32 ///     T        = A range. If parsing, this must be an input range, or if serializing, an output range.
33 ///     isParser = If true this should be a parser
34 struct RCBin(T, bool isParser) {
35 
36     // Checks for parser
37     static if (isParser) {
38 
39         static assert(isInputRange!T, "RCBin parser argument is not an input range.");
40         static assert(is(ElementType!T == ubyte), "RCBin range must have ubyte as element type.");
41         enum isSerializer = false;
42 
43     }
44 
45     // Checks for serializer
46     else {
47 
48         static assert(isOutputRange!(T, ubyte), "RCBin serializer argument is not an output range.");
49         enum isSerializer = true;
50 
51     }
52 
53 
54     /// Range to read or write to.
55     T* range;
56 
57     /// Create a parser or serializer with the given range.
58     this(ref T range) {
59 
60         this.range = ⦥
61 
62     }
63 
64     /// Get a value.
65     /// Returns: the current `RCBin` instance to allow chaining
66     static if (isParser)
67     RCBin get(T)(ref T target) {
68 
69         return getImpl(target);
70 
71     }
72 
73     /// Ditto
74     static if (isSerializer)
75     RCBin get(T)(const T input) {
76 
77         // Safe cast: getImpl won't modify the clone
78         T clone = cast(T) input;
79         return getImpl(clone);
80 
81     }
82 
83     private RCBin getImpl(T)(ref T target) {
84 
85         // Boolean
86         static if (is(T == bool)) getBoolean(target);
87 
88         // Basic types
89         else static if (isNumeric!T || isSomeChar!T) getNumber(target);
90 
91         // Arrays
92         else static if (isArray!T) getArray(target);
93 
94         // Structs
95         else static if (is(T == struct)) getStruct(target);
96 
97         // Other types
98         else static assert(0, "Unsupported type " ~ fullyQualifiedName!T);
99 
100         return this;
101 
102     }
103 
104     /// Read and return a value. Should be avoided to enable making serializers.
105     static if (isParser)
106     T read(T)() {
107 
108         T value;
109         get(value);
110         return value;
111 
112     }
113 
114     /// Read or write a boolean to the stream.
115     void getBoolean(ref bool target) {
116 
117         // Read the value
118         ubyte tempTarget = target;
119         getNumber(tempTarget);
120         target = cast(bool) tempTarget;
121 
122     }
123 
124 
125     /// Read or write a number to the stream.
126     static if (isParser)
127     void getNumber(T)(ref T target)
128     if (isNumeric!T || isSomeChar!T) {
129 
130         // Read the value
131         target = takeExactly(*range, T.sizeof)
132             .staticArray!(T.sizeof)
133             .littleEndianToNative!T;
134 
135         // Advance the range
136         popFrontN(*range, T.sizeof);
137 
138     }
139 
140     /// Ditto
141     static if (isSerializer)
142     void getNumber(T)(const T input)
143     if (isNumeric!T || isSomeChar!T) {
144 
145         // Write the value
146         multiput(*range, input.nativeToLittleEndian[]);
147 
148     }
149 
150     /// Read or write a dynamic array to the stream.
151     static if (isParser)
152     void getArray(T)(ref T[] target) {
153 
154         // Make a new array in case T is const
155         Unconst!T[] clone;
156 
157         // Get array length
158         clone.length = cast(size_t) read!ulong;
159 
160         // Fill each item
161         foreach (ref item; clone) {
162 
163             item = read!(Unconst!T);
164 
165         }
166 
167         // Apply to target
168         target = cast(T[]) clone;
169 
170     }
171 
172     /// Ditto
173     static if (isSerializer)
174     void getArray(T)(const T[] input) {
175 
176         // Write array length
177         get(cast(ulong) input.length);
178         // target.length would be uint on 32 bit machines, so we cast it to ulong
179 
180         // Write each item
181         foreach (item; input) {
182 
183             get(item);
184 
185         }
186 
187     }
188 
189     /// Read or write a static array to the stream.
190     static if (isParser)
191     void getArray(T, size_t size)(ref T[size] target) {
192 
193         // TODO const(T) support? if that works for static arrays...
194 
195         foreach (ref item; target) {
196 
197             item = read!T;
198 
199         }
200 
201     }
202 
203     /// Ditto
204     static if (isSerializer)
205     void getArray(T, size_t size)(const T[size] input) {
206 
207         // TODO const(T) support? if that works for static arrays...
208 
209         foreach (item; input) {
210 
211             get(item);
212 
213         }
214 
215     }
216 
217     /// Read or write all struct fields.
218     static if (isParser)
219     void getStruct(T)(ref T target)
220     if (is(T == struct)) {
221 
222         // Iterate on each field
223         static foreach (fieldName; FieldNameTuple!T) {
224 
225             get(mixin("target." ~ fieldName));
226 
227         }
228 
229     }
230 
231     /// Ditto
232     static if (isSerializer)
233     void getStruct(T)(const T target)
234     if (is(T == struct)) {
235 
236         // Iterate on each field
237         static foreach (fieldName; FieldNameTuple!T) {
238 
239             get(mixin("target." ~ fieldName));
240 
241         }
242 
243     }
244 
245 }
246 
247 ///
248 unittest {
249 
250     struct Foo {
251 
252         int id;
253         float value;
254         string name;
255         string[] arguments;
256         string unicodeString;
257         int[] numbers;
258         int[3] staticArray;
259 
260     }
261 
262     void getData(T)(ref T bin, ref Foo target)
263     if (isRCBin!T) {
264 
265         bin.get(target.id)
266            .get(target.value)
267            .get(target.name)
268            .get(target.arguments)
269            .get(target.unicodeString)
270            .get(target.numbers)
271            .get(target.staticArray);
272 
273     }
274 
275     Foo foo = {
276         id: 123,
277         value: 42.01,
278         name: "John Doe",
279         arguments: ["a", "ab", "b"],
280         unicodeString: "Ich fühle mich gut.",
281         numbers: [1, 2, 3, 4],
282         staticArray: [1, 2, 3],
283     };
284 
285     // Serialize the data
286     auto data = appender!(ubyte[]);
287     auto serializer = rcbinSerializer(data);
288     getData(serializer, foo);
289 
290     // Read the data
291     ubyte[] buffer = data[];
292     Foo newFoo;
293     auto parser = rcbinParser(buffer);
294     getData(parser, newFoo);
295 
296     assert(newFoo.id == 123);
297     assert(newFoo.value == 42.01f);
298     assert(newFoo.name == "John Doe");
299     assert(newFoo.arguments == ["a", "ab", "b"]);
300     assert(newFoo.unicodeString == "Ich fühle mich gut.");
301     assert(newFoo.numbers == [1, 2, 3, 4]);
302     assert(newFoo.staticArray == [1, 2, 3]);
303 
304     // Or even better — serialize the whole struct
305     auto data2 = appender!(ubyte[]);
306     rcbinSerializer(data2)
307         .getStruct(foo);
308 
309     auto buffer2 = data2[];
310     assert(data[] == buffer2);
311 
312     // And read the data later
313     Foo anotherFoo;
314     rcbinParser(buffer2)
315         .getStruct(anotherFoo);
316 
317     assert(foo == anotherFoo);
318 
319 }
320 
321 unittest {
322 
323     char test = 'a';
324 
325     auto data = appender!(ubyte[]);
326     rcbinSerializer(data)
327         .get(test);
328     auto buffer = data[];
329 
330     char newTest;
331     auto parser = rcbinParser(buffer)
332         .get(newTest);
333 
334     assert(newTest == 'a');
335 
336 }
337 
338 unittest {
339 
340     auto data = appender!(ubyte[]);
341     rcbinSerializer(data)
342         .get(true)
343         .get(false);
344 
345     auto buffer = data[];
346 
347     bool foo, bar;
348     auto parser = rcbinParser(buffer)
349         .get(foo)
350         .get(bar);
351 
352     assert(foo);
353     assert(!bar);
354     assert(buffer.length == 0);
355 
356 }
357 
358 // std.range's put sucks and does some really unexpected and weird behavior for arrays
359 /// Append input to output. Input can be a single value or a range.
360 private void multiput(R, E)(ref R output, E input) {
361 
362     // Add all array items
363     static if (isInputRange!E) {
364 
365         foreach (element; input) {
366 
367             output.put(element);
368 
369         }
370 
371     }
372 
373     // Add one element
374     else output.put(input);
375 
376 }