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 }