1 /**
2 Primitives used for type (de)serialization.
3 
4 Copyright: Copyright Boris-Barboris 2017.
5 License: MIT
6 Authors: Boris-Barboris
7 */
8 
9 module dpeq.serialize;
10 
11 import std.algorithm: canFind, min;
12 import std.exception: enforce;
13 import std.bitmanip: nativeToBigEndian, bigEndianToNative;
14 import std.conv: to;
15 import std.meta;
16 import std.traits;
17 public import std.typecons: Nullable, Tuple;
18 import std.variant;
19 import std.uuid: UUID;
20 
21 import dpeq.constants;
22 import dpeq.exceptions;
23 
24 
25 
26 /// Value type descriptor, constisting of an oid of the type itself and a
27 /// boolean flag wich indicates wether the value can be null.
28 struct FieldSpec
29 {
30     OID typeId;
31     bool nullable;
32 }
33 
34 
35 /**
36 Serializing function converts value pointed to by val to PSQL network protocol format,
37 writing it to byte buffer 'to' and returning number of bytes written, -1 if the value
38 is null, and -2 if 'to' is too small.
39 If result is less than -2, absolute value is the amount of bytes that is
40 required to fully serialize the value.
41 */
42 alias SerializeF = int function(scope ubyte[] to, scope const void* val)
43     nothrow pure @safe;
44 
45 /**
46 Deserializing function reads 'len' bytes and writes the conversion result to the
47 value, pointed by 'val'.
48 */
49 alias DeserializeF = void function(immutable(ubyte)[] from, in FormatCode fc,
50     in int len, scope void* val) pure @safe;
51 
52 
53 /** Default compile-time one-to-many mapper, wich for OID of some Postgres type
54 * gives it's native type representation, and serializing and deserializing
55 * functions. You can extend it with two custom mappers: Pre and Post. */
56 template DefaultSerializer(FieldSpec field, alias Pre = NopSerializer,
57     alias Post = StringSerializer)
58 {
59     static if (Pre!field.canDigest)
60     {
61         // must be an alias to D type representing this value
62         alias type = Pre!field.type;
63 
64         // default format code for this type
65         enum FormatCode formatCode = Pre!field.formatCode;
66 
67         // deserializer should accept all formatCodes that make sense for this
68         // type, and it must throw in unhandled case.
69         immutable DeserializeF deserialize = cast(DeserializeF) &Pre!field.deserialize;
70 
71         // serializer only needs to support one formatCode, mentioned above
72         immutable SerializeF serialize = cast(SerializeF) &Pre!field.serialize;
73     }
74     else static if (StaticFieldSerializer!field.canDigest)
75     {
76         alias type = StaticFieldSerializer!field.type;
77         enum FormatCode formatCode = StaticFieldSerializer!field.formatCode;
78         immutable DeserializeF deserialize = cast(DeserializeF) &StaticFieldSerializer!field.deserialize;
79         immutable SerializeF serialize = cast(SerializeF) &StaticFieldSerializer!field.serialize;
80     }
81     else static if (Post!field.canDigest)
82     {
83         alias type = Post!field.type;
84         enum FormatCode formatCode = Post!field.formatCode;
85         immutable DeserializeF deserialize = cast(DeserializeF) &Post!field.deserialize;
86         immutable SerializeF serialize = cast(SerializeF) &Post!field.serialize;
87     }
88     else
89         static assert(0, "Unknown typeId " ~ field.typeId.to!string ~
90             ", cannot (de)serialize");
91 }
92 
93 /// Can't serialize shit
94 template NopSerializer(FieldSpec type)
95 {
96     enum canDigest = false;
97 }
98 
99 /// This is a fallback serializeler that simply accepts any type as a string
100 template StringSerializer(FieldSpec field)
101 {
102     mixin SerialTemplate!(string, FormatCode.Text, "StringField");
103 }
104 
105 /// Types wich are well-known to dpeq
106 template StaticFieldSerializer(FieldSpec field)
107 {
108     static if (field.typeId == StaticPgTypes.BOOLEAN)
109         mixin SerialTemplate!(bool, FormatCode.Binary, "FixedField");
110     else static if (field.typeId == StaticPgTypes.BIGINT)
111         mixin SerialTemplate!(long, FormatCode.Binary, "FixedField");
112     else static if (field.typeId == StaticPgTypes.SMALLINT)
113         mixin SerialTemplate!(short, FormatCode.Binary, "FixedField");
114     else static if (field.typeId == StaticPgTypes.INT)
115         mixin SerialTemplate!(int, FormatCode.Binary, "FixedField");
116     else static if (field.typeId == StaticPgTypes.OID)
117         mixin SerialTemplate!(int, FormatCode.Binary, "FixedField");
118     else static if (field.typeId == StaticPgTypes.VARCHAR)
119         mixin SerialTemplate!(string, FormatCode.Text, "StringField");
120     else static if (field.typeId == StaticPgTypes.CHARACTER)
121         mixin SerialTemplate!(string, FormatCode.Text, "StringField");
122     else static if (field.typeId == StaticPgTypes.TEXT)
123         mixin SerialTemplate!(string, FormatCode.Text, "StringField");
124     else static if (field.typeId == StaticPgTypes.UUID)
125         mixin SerialTemplate!(UUID, FormatCode.Binary, "UuidField");
126     else static if (field.typeId == StaticPgTypes.REAL)
127         mixin SerialTemplate!(float, FormatCode.Binary, "FixedField");
128     else static if (field.typeId == StaticPgTypes.DOUBLE)
129         mixin SerialTemplate!(double, FormatCode.Binary, "FixedField");
130     else
131         enum canDigest = false;
132 }
133 
134 mixin template SerialTemplate(NativeT, FormatCode fcode, string suffix)
135 {
136     enum canDigest = true;
137     enum FormatCode formatCode = fcode;
138     static if (field.nullable)
139     {
140         alias type = Nullable!NativeT;
141         mixin("alias deserialize = deserializeNullable" ~ suffix ~ "!(NativeT);");
142         mixin("alias serialize = serializeNullable" ~ suffix ~ "!(NativeT);");
143     }
144     else
145     {
146         alias type = NativeT;
147         mixin("alias deserialize = deserialize" ~ suffix ~ "!(NativeT);");
148         mixin("alias serialize = serialize" ~ suffix ~ "!(NativeT);");
149     }
150 }
151 
152 
153 template FCodeOfFSpec(alias Serializer = DefaultSerializer)
154 {
155     template F(FieldSpec spec)
156     {
157         enum F = Serializer!spec.formatCode;
158     }
159 }
160 
161 /// Utility template to quickly get an array of format codes from an array of
162 /// FieldSpecs
163 template FSpecsToFCodes(FieldSpec[] specs, alias Serializer = DefaultSerializer)
164 {
165     enum FSpecsToFCodes = [staticMap!(FCodeOfFSpec!Serializer.F, aliasSeqOf!specs)];
166 }
167 
168 /*
169 ///////////////////////////////////////////////////////////////////////////
170 // Serializer implementations. Serializer writes only body of the data
171 // and returns count of bytes written, -1 if it's a null value,
172 // and -2 if the buffer is too small to fit the whole value.
173 ///////////////////////////////////////////////////////////////////////////
174 */
175 
176 @safe pure nothrow
177 {
178 
179     pragma(inline, true)
180     int serializeNull(scope ubyte[] to)
181     {
182         return -1;  // special case, -1 length is null value in eq protocol.
183     }
184 
185     // I don't really know how versatile are these functions, so let's keep
186     // them FixedField instead of NumericField
187 
188     int serializeNullableFixedField(T)(scope ubyte[] to,
189         scope const(Nullable!T)* ptr) @trusted
190     {
191         if (ptr.isNull)
192             return serializeNull(to);
193         return serializeFixedField!T(to, &(ptr.get()));
194     }
195 
196     int serializeFixedField(T)(scope ubyte[] to, scope const T* val)
197     {
198         if (T.sizeof > to.length)
199             return -2;
200         auto arr = nativeToBigEndian!T(*val);
201         to[0 .. arr.length] = arr;
202         return arr.length;
203     }
204 
205     int serializeNullableStringField(Dummy = void)(scope ubyte[] to,
206         scope const(Nullable!string)* val) @trusted
207     {
208         if (val.isNull)
209             return serializeNull(to);
210         return serializeStringField(to, &(val.get()));
211     }
212 
213     int serializeStringField(Dummy = void)(scope ubyte[] to,
214         scope const string* val)
215     {
216         if (val.length > int.max - 1)
217             assert(0, "string too long");
218         if (val.length > to.length)
219             return min(-2, -val.length);
220         for (int i = 0; i < val.length; i++)
221             to[i] = cast(const(ubyte)) (*val)[i];
222         return cast(int) val.length;
223     }
224 
225     int serializeBytesField(Dummy = void)(scope ubyte[] to,
226         scope const(ubyte[])* val)
227     {
228         if (val.length > int.max - 1)
229             assert(0, "array too long");
230         if (val.length > to.length)
231             return min(-2, -val.length);
232         to[0..val.length] = (*val)[];
233         return cast(int) val.length;
234     }
235 
236     /// Service function, used for serializeling of protocol messages.
237     /// Data strings are passed without trailing nulls.
238     int serializeCstring(scope ubyte[] to, scope const string s)
239     {
240         if (s.length + 1 > int.max - 1)
241             assert(0, "string too long");
242         if (s.length + 1 > to.length)
243             return min(-2, -s.length);
244         for (int i = 0; i < s.length; i++)
245             to[i] = cast(const(ubyte)) s[i];
246         to[s.length] = cast(ubyte)0;
247         return cast(int)(s.length + 1);
248     }
249 
250     int serializeNullableUuidField(Dummy = void)(scope ubyte[] to,
251         scope const(Nullable!UUID)* val) @trusted
252     {
253         if (val.isNull)
254             return serializeNull(to);
255         return serializeUuidField(to, &(val.get()));
256     }
257 
258     int serializeUuidField(Dummy = void)(scope ubyte[] to,
259         scope const UUID* val)
260     {
261         if (to.length < 16)
262             return -2;
263         for (int i = 0; i < 16; i++)
264             to[i] = val.data[i];
265         return 16;
266     }
267 
268 }
269 
270 /*
271 //////////////////////////////////////////////////////////////////////////////
272 // Deserialing implementations. Deserializers take byte array that contains
273 // field data, it's format code and length, and return the resulting value or throw.
274 //////////////////////////////////////////////////////////////////////////////
275 */
276 
277 @safe pure
278 {
279 
280     /// Service function. Simple deserialize of some numeric type.
281     pragma(inline)
282     T deserializeNumber(T = int)(scope immutable(ubyte)[] from) nothrow
283         if (isNumeric!T)
284     {
285         return bigEndianToNative!T(from[0 .. T.sizeof]);
286     }
287 
288     /// psql uses `t` and `f` for boolean
289     private bool to(T: bool)(in string s)
290     {
291         if (s == "t")
292             return true;
293         if (s == "f")
294             return false;
295         throw new PsqlSerializationException("Unable to deserialize bool from string " ~ s);
296     }
297 
298     void deserializeFixedField(T)(scope immutable(ubyte)[] from, in FormatCode fCode,
299         in int len, scope T* val)
300     {
301         if (val is null)
302             assert(0, "null value pointer");
303         enforce!PsqlSerializationException(len != -1, "null in non-null deserializer");
304         enforce!PsqlSerializationException(len > 0, "zero-sized fixed field");
305         if (fCode == FormatCode.Binary)
306         {
307             enforce!PsqlSerializationException(len == T.sizeof, "Field size mismatch");
308             *val = bigEndianToNative!T(from[0 .. T.sizeof]);
309         }
310         else if (fCode == FormatCode.Text)
311             *val = deserializeString(from[0 .. len]).to!T;
312         else
313             throw new PsqlSerializationException(
314                 "Unsupported FormatCode " ~ short(fCode).to!string);
315     }
316 
317     void deserializeNullableFixedField(T)(scope immutable(ubyte)[] from,
318         in FormatCode fCode, in int len, scope Nullable!T *val) @trusted
319     {
320         if (val is null)
321             assert(0, "null value pointer");
322         if (len == -1)
323         {
324             *val = Nullable!T();
325             return;
326         }
327         T res;
328         deserializeFixedField!T(from, fCode, len, &res);
329         *val = Nullable!T(res);
330     }
331 
332     void deserializeStringField(Dummy = void)(immutable(ubyte)[] from,
333         in FormatCode fc, in int len, scope string* val)
334     {
335         if (val is null)
336             assert(0, "null value pointer");
337         enforce!PsqlSerializationException(len != -1, "null in non-null deserializer");
338         if (len == 0)
339         {
340             *val = "";
341             return;
342         }
343         *val =  cast(string) from[0 .. len.to!size_t];
344     }
345 
346     /// returns inplace-constructed string without allocations. Hacky.
347     void deserializeNullableStringField(Dummy = void)(immutable(ubyte)[] from,
348         in FormatCode fc, in int len, scope Nullable!string *val) @trusted
349     {
350         if (val is null)
351             assert(0, "null value pointer");
352         if (len == -1)
353         {
354             *val = Nullable!string();
355             return;
356         }
357         string res;
358         deserializeStringField(from, fc, len, &res);
359         *val = Nullable!string(res);
360     }
361 
362     /// dpeq utility function
363     string deserializeString(immutable(ubyte)[] from) nothrow
364     {
365         return cast(string) from[0 .. from.length];
366     }
367 
368     /// dpeq utility function. Deserialize zero-terminated string from byte buffer.
369     string deserializeProtocolString(immutable(ubyte)[] from, out size_t length)
370     {
371         size_t l = 0;
372         while (from[l])
373         {
374             l++;
375             if (l >= from.length)
376                 throw new PsqlSerializationException("Null-terminated string is not " ~
377                     "null-terminated");
378         }
379         length = l + 1;
380         if (l == 0)
381             return string.init;
382         return deserializeString(from[0..l]);
383     }
384 
385     void deserializeNullableUuidField(Dummy = void)(scope immutable(ubyte)[] from,
386         in FormatCode fc, in int len, scope Nullable!UUID *val) @trusted
387     {
388         if (val is null)
389             assert(0, "null value pointer");
390         if (len == -1)
391         {
392             *val = Nullable!UUID();
393             return;
394         }
395         UUID res;
396         deserializeUuidField(from, fc, len, &res);
397         *val = Nullable!UUID(res);
398     }
399 
400     void deserializeUuidField(Dummy = void)(scope immutable(ubyte)[] from,
401         in FormatCode fc, in int len, scope UUID* val)
402     {
403         if (val is null)
404             assert(0, "null value pointer");
405         enforce!PsqlSerializationException(len != -1,
406             "null uuid in non-null deserializer");
407         if (fc == FormatCode.Binary)
408         {
409             enforce!PsqlSerializationException(len == 16, "uuid is not 16-byte");
410             ubyte[16] data = from[0..16];
411             *val =  UUID(data);
412         }
413         else if (fc == FormatCode.Text)
414         {
415             string s = cast(string) from[0 .. len.to!size_t];
416             *val = UUID(s);
417         }
418         else
419             throw new PsqlSerializationException(
420                 "Unsupported FormatCode " ~ short(fc).to!string);
421     }
422 
423 }
424 
425 
426 /*
427 /////////////////////////////////////////////////////////////////
428 // Dynamic conversion code, suitable for dynamic typing
429 /////////////////////////////////////////////////////////////////
430 */
431 
432 /// prototype of a nullable variant deserializer, used in converter
433 alias VariantDeserializer =
434     NullableVariant function(scope immutable(ubyte)[] buf, in FormatCode fc, in int len) @trusted;
435 
436 /// std.variant.Variant subtype that is better suited for holding SQL null.
437 /// Null NullableVariant is essentially a valueless Variant instance.
438 struct NullableVariant
439 {
440     Variant variant;
441     alias variant this;
442 
443     this(T)(T value)
444     {
445         variant = value;
446     }
447 
448     /// this property will be true if psql return null in this column
449     bool isNull() const pure nothrow @safe { return !variant.hasValue; }
450 
451     string toString() @system
452     {
453         if (isNull)
454             return "null";
455         else
456             return variant.toString();
457     }
458 
459     NullableVariant opAssign(typeof(null) n) @system
460     {
461         variant = Variant();
462         return this;
463     }
464 }
465 
466 unittest
467 {
468     NullableVariant nv = NullableVariant("asd");
469     assert(!nv.isNull);
470     nv = null;
471     assert(nv.isNull);
472 }
473 
474 /// Default converter hash map. You can extend it, or define your own.
475 abstract class VariantConverter(alias Serializer = DefaultFieldSerializer)
476 {
477     static immutable VariantDeserializer[OID] deserializers;
478 
479     @disable private this();
480 
481     shared static this()
482     {
483         VariantDeserializer[OID] aa;
484         // iterate over StaticPgTypes and take deserializers from StaticFieldSerializer
485         foreach (em; __traits(allMembers, StaticPgTypes))
486         {
487             // this assumes nullable return fields. Variant will wrap Nullable
488             // of some native type.
489             enum FieldSpec spec =
490                 FieldSpec(__traits(getMember, StaticPgTypes, em), true);
491             alias NullableT = Serializer!spec.type;
492             aa[spec.typeId] =
493                 (scope immutable(ubyte)[] buf, in FormatCode fc, in int len) @trusted
494                 {
495                     NullableT result;
496                     Serializer!spec.deserialize(buf, fc, len, &result);
497                     if (result.isNull)
498                         return NullableVariant();
499                     else
500                         return NullableVariant(result.get());
501                 };
502         }
503         deserializers = cast(immutable VariantDeserializer[OID]) aa;
504     }
505 
506     static NullableVariant deserialize(
507         immutable(ubyte)[] fieldBody, OID type, FormatCode fc, int len) @system
508     {
509         immutable(VariantDeserializer)* func = type in deserializers;
510         if (func)
511             return (*func)(fieldBody, fc, len);
512         else
513         {
514             // fallback to nullable string deserializer
515             if (fc == FormatCode.Text)
516                 return deserializers[StaticPgTypes.VARCHAR](fieldBody, fc, len);
517             else
518                 throw new PsqlSerializationException(
519                     "Unable to deduce deserializer for binary format of a type " ~
520                     type.to!string);
521         }
522     }
523 }