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