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 }