1 /** Hash digestion of standard types.
2  *
3  * TODO use:
4  *
5  *  static if (hasMember!(hasher, "putStaticArray"))
6  *  {
7  *      digest.putStaticArray((cast(ubyte*)&value)[0 .. value.sizeof]);
8  *  }
9  *  else
10  *  {
11  *      digest.put((cast(ubyte*)&value)[0 .. value.sizeof]);
12  *  }
13  */
14 module nxt.digestion;
15 
16 import std.traits : hasMember, isScalarType, hasIndirections, isArray, isPointer;
17 import std.digest : isDigest;
18 import nxt.container_traits : isAddress;
19 
20 @safe:
21 
22 /// ditto
23 hash_t hashOf2(alias hasher, T)(const scope auto ref T value)
24 {
25     version(LDC) pragma(inline, true);
26     static if (__traits(compiles, { enum _ = isDigest!hasher; }) &&
27                isDigest!hasher &&
28                hasMember!(hasher, "get"))
29     {
30         import std.digest : makeDigest;
31 
32         auto digest = makeDigest!(hasher);
33         static if (hasMember!(T, "toDigest"))
34         {
35             value.toDigest(digest);
36         }
37         else
38         {
39             digestAny(digest, value);
40         }
41         digest.finish();
42         auto result = digest.get();
43 
44         static if (is(typeof(result) == typeof(return)))
45         {
46             return result;
47         }
48         else static if (is(typeof(result) == typeof(return)[2]))
49         {
50             return (result[0] ^
51                     result[1]);
52         }
53         else
54         {
55             static assert(0, "Handle get() with return type " ~ typeof(result).stringof ~
56                           " on " ~ size_t.sizeof.stringof ~ "-bit platform");
57         }
58     }
59     else static if (__traits(compiles, { auto _ = hasher((ubyte[]).init); }))
60     {
61         // cast input `value` to `ubyte[]` and use std.digest API
62         immutable digest = hasher((cast(ubyte*)&value)[0 .. value.sizeof]); // TODO ask forums when this isn't correct
63 
64         static assert(digest.sizeof >=
65                       typeof(return).sizeof,
66                       `Size of digest is ` ~ digest.sizeof
67                       ~ ` but needs to be at least ` ~ typeof(return).sizeof.stringof);
68 
69         import std.traits : isUnsigned, isStaticArray;
70 
71         static if (isUnsigned!(typeof(digest)))
72         {
73             return cast(typeof(return))digest; // fast modulo calculation
74         }
75         else static if (isStaticArray!(typeof(digest)))
76         {
77             typeof(return) hashIndex;
78 
79             static if (2*hash_t.sizeof == digest.sizeof)
80             {
81                 // for instance, use all 128-bits when hash_t is 64-bit
82                 (cast(ubyte*)&hashIndex)[0 .. hashIndex.sizeof] = (digest[0 .. hashIndex.sizeof] ^
83                                                                    digest[hashIndex.sizeof .. 2*hashIndex.sizeof]);
84             }
85             else
86             {
87                 (cast(ubyte*)&hashIndex)[0 .. hashIndex.sizeof] = digest[0 .. hashIndex.sizeof];
88             }
89 
90             return hashIndex;
91         }
92         else
93         {
94             static assert(0, "Unsupported digest type " ~ typeof(digest).stringof);
95         }
96     }
97     else
98     {
99         static assert(0, "Cannot combine hasher " ~ hasher.stringof ~
100                       " with element type " ~ T.stringof);
101     }
102 }
103 
104 /** Digest `value` into `digest`.
105  */
106 void digestAny(Digest, T)(ref Digest digest,
107                           const scope auto ref T value)
108 if (isDigest!Digest)
109 {
110     version(LDC) pragma(inline, true);
111     static if (isScalarType!T)  // first because faster to evaluate than
112                                 // `!hasIndirections!T` below
113     {
114         digestRaw(digest, value);
115     }
116     else static if (hasMember!(T, "toDigest"))
117     {
118         value.toDigest(digest);
119     }
120     else static if (isAddress!T)
121     {
122         digestAddress(digest, value);
123     }
124     else static if (!hasIndirections!T) // no pointers left in `T`. TODO make this the default in-place of `isScalarType`
125     {
126         version(LDC)            // LDC doesn't zero pad `real`s
127         {
128             // TODO needed to improve this with a trait
129             import std.traits : isStaticArray, isInstanceOf;
130             static if (isStaticArray!T)
131             {
132                 import std.meta : staticIndexOf;
133                 enum isReal = is(typeof(T.init[0]) == real);
134                 static if (isReal) // static array of `real`s
135                 {
136                     foreach (e; value)
137                     {
138                         digestRaw(digest, e); // hash each element for now because padding might now be zero
139                     }
140                 }
141                 else
142                 {
143                     digestRaw(digest, value); // hash everything in one call for better speed
144                 }
145             }
146             else
147             {
148                 import std.typecons : Nullable;
149                 static if (isInstanceOf!(Nullable, T))
150                 {
151                     digestRaw(digest, value); // hash everything in one call for better speed
152                 }
153                 else
154                 {
155                     import std.meta : staticIndexOf;
156                     enum realIndex = staticIndexOf!(real, typeof(T.init.tupleof));
157                     static if (realIndex != -1)
158                     {
159                         digestStruct(digest, value); // hash each element for now because padding might now be zero
160                     }
161                     else
162                     {
163                         digestRaw(digest, value); // hash everything in one call for better speed
164                     }
165                 }
166             }
167         }
168         else
169         {
170             digestRaw(digest, value); // hash everything in one call for better speed
171         }
172     }
173     else static if (isArray!T) // including `T` being `string`, `wstring`, `dstring`
174     {
175         digestArray(digest, value);
176     }
177     else static if (is(T == struct))
178     {
179         static if (is(typeof(T.init[])) && isArray!(typeof(T.init[]))) // TODO trait: `isArrayLike`
180         {
181             digestAnyWithTrustedSystemSlicing(digest, value);
182         }
183         else
184         {
185             digestStruct(digest, value);
186         }
187     }
188     else
189     {
190         static assert(0, "handle type " ~ T.stringof);
191     }
192 }
193 
194 // TODO remove when `SSOString` gets scope-checked opSlice
195 private
196 void digestAnyWithTrustedSystemSlicing(Digest, T)(ref Digest digest,
197                                                   const scope ref T value) @trusted
198 {
199     digestArray(digest, value[]);
200 }
201 
202 /** Digest the `value` as an address (pointer). */
203 private void digestAddress(Digest, T)(scope ref Digest digest,
204                                       const scope T value) @trusted // pointer passed by value
205 if (isDigest!Digest &&
206     isAddress!T)
207 {
208     static if (is(T == class))
209     {
210         static if (size_t.sizeof == 8)
211         {
212             enum bitshift = 3;  // 64-bit platform
213         }
214         else
215         {
216             enum bitshift = 2;  // 32-bit platform
217         }
218     }
219     else static if (isPointer!T)
220     {
221         enum Tvalue = typeof(*T.init);
222         enum Talignment = T.value.alignof;
223         static      if (Talignment == 16)
224         {
225             enum bitshift = 4;
226         }
227         else static if (Talignment == 8)
228         {
229             enum bitshift = 3;
230         }
231         else static if (Talignment == 4)
232         {
233             enum bitshift = 2;
234         }
235         else static if (Talignment == 2)
236         {
237             enum bitshift = 1;
238         }
239         else static if (Talignment == 1)
240         {
241             enum bitshift = 0;
242         }
243         else
244         {
245             static assert(0, "Cannot calculate alignment of T being " ~ T.stringof);
246         }
247     }
248     const valueAsSize = *cast(size_t*)(&value); // `value` as pointer
249     assert((valueAsSize & (bitshift - 1)) == 0); // shifted out bits should all be zero
250     digestRaw(digest, valueAsSize >> bitshift);
251 }
252 
253 /** Digest the struct `value` by digesting each member sequentially. */
254 private void digestStruct(Digest, T)(scope ref Digest digest,
255                                      const scope auto ref T value) @trusted
256 if (isDigest!Digest &&
257     is(T == struct))
258 {
259     static if (!hasIndirections!T)
260     {
261         version(D_Coverage) {} else pragma(inline, true);
262         digestRaw(digest, value); // hash everything in one call for better speed
263     }
264     else
265     {
266         version(LDC) pragma(inline, true);
267         foreach (const ref subValue; value.tupleof) // for each member
268         {
269             digestAny(digest, subValue);
270         }
271     }
272 }
273 
274 /** Digest the array `value`. */
275 void digestArray(Digest, T)(scope ref Digest digest,
276                             const scope auto ref T value) @trusted
277 if (isDigest!Digest &&
278     isArray!T)
279 {
280     import std.traits : isDynamicArray;
281     static if (isDynamicArray!T)
282     {
283         // only dynamic arrays vary in length for a specific type `T` being
284         // hashed
285         digestRaw(digest, value.length); // length
286     }
287 
288     alias E = typeof(T.init[0]);
289     static if (isScalarType!E ||
290                isPointer!E ||
291                (is(T == class) &&
292                 !hasMember!(T, "toDigest")))
293     {
294         version(D_Coverage) {} else pragma(inline, true);
295         digest.put((cast(ubyte*)value.ptr)[0 .. value.length * value[0].sizeof]); // faster
296     }
297     else
298     {
299         foreach (const ref element; value)
300         {
301             digestAny(digest, element); // slower
302         }
303     }
304 }
305 
306 /** Digest raw bytes of `values`.
307  */
308 private void digestRaw(Digest, T)(scope ref Digest digest,
309                                   const scope T value) @trusted // pass scalar types by value
310 if (isDigest!Digest &&
311     isScalarType!T)             // scalar type `T`
312 {
313     version(LDC) pragma(inline, true);
314     // TODO optimize when value is size_t, ulong and digest supports it
315     digest.put((cast(ubyte*)&value)[0 .. value.sizeof]);
316 }
317 
318 private void digestRaw(Digest, T)(scope ref Digest digest,
319                                   const scope auto ref T value) @trusted // pass non-scalar types by ref when possible
320 if (isDigest!Digest &&
321     !isScalarType!T)            // non-scalar type `T`
322 {
323     version(LDC) pragma(inline, true);
324     // TODO optimize when value is size_t, ulong and digest supports it
325     digest.put((cast(ubyte*)&value)[0 .. value.sizeof]);
326 }
327 
328 /// arrays and containers and its slices
329 @safe pure unittest
330 {
331     import nxt.dynamic_array : DynamicArray;
332 
333     alias E = double;
334 
335     immutable e = [cast(E)1, cast(E)2, cast(E)3].s;
336     auto a = DynamicArray!E.withElements(e.s);
337 
338     // static array and its slice (dynamic array) hash differently
339     const sh = hashOf2!(FNV64)(e); /* does not need to include length in hash
340                                     * because all instances of typeof(e) have
341                                     * the same length */
342     const dh = hashOf2!(FNV64)(e[]); // includes hash in length
343     assert(sh != dh);
344 
345     // array container and its slice should hash equal
346     assert(hashOf2!(FNV64)(a) ==
347            hashOf2!(FNV64)(e[]));
348 }
349 
350 @trusted pure unittest
351 {
352     const ubyte[8] bytes8 = [1, 2, 3, 4, 5, 6, 7, 8];
353     assert(hashOf2!(FNV64)(bytes8) == 9130222009665091821UL);
354 
355     enum E { first, second, third }
356 
357     assert(hashOf2!(FNV64)(E.first) ==
358            hashOf2!(FNV64)(E.first));
359     assert(hashOf2!(FNV64)(E.second) ==
360            hashOf2!(FNV64)(E.second));
361     assert(hashOf2!(FNV64)(E.third) ==
362            hashOf2!(FNV64)(E.third));
363     assert(hashOf2!(FNV64)(E.first) !=
364            hashOf2!(FNV64)(E.second));
365     assert(hashOf2!(FNV64)(E.second) !=
366            hashOf2!(FNV64)(E.third));
367 
368     struct V
369     {
370         float f = 3;
371         double d = 5;
372         real r = 7;
373         E e;
374     }
375 
376     struct S
377     {
378         string str = `abc`;
379         wstring wstr = `XYZ`;
380         dstring dstr = `123`;
381         size_t sz = 17;
382         ushort us = 18;
383         ubyte ub = 255;
384         V v;
385     }
386 
387     // check determinism
388     assert(hashOf2!(FNV64)("alpha") ==
389            hashOf2!(FNV64)("alpha".dup));
390 
391     assert(hashOf2!(FNV64)(S()) ==
392            hashOf2!(FNV64)(S()));
393 
394     struct HasDigest
395     {
396         E e;
397         void toDigest(Digest)(scope ref Digest digest) const
398             pure nothrow @nogc
399             if (isDigest!Digest)
400         {
401             digestRaw(digest, e);
402         }
403     }
404 
405     assert(hashOf2!(FNV64)(HasDigest(E.first)) ==
406            hashOf2!(FNV64)(HasDigest(E.first)));
407 
408     assert(hashOf2!(FNV64)(HasDigest(E.first)) !=
409            hashOf2!(FNV64)(HasDigest(E.second)));
410 }
411 
412 version(unittest)
413 {
414     import nxt.digestx.fnv : FNV;
415     alias FNV64 = FNV!(64, true);
416     import nxt.dbgio;
417     import nxt.array_help : s;
418 }