1 module nxt.digest_ex;
2 
3 /** Message Digest.
4 
5 	Zeros contents means uninitialized digest.
6 
7 	Copyright: Per Nordlöw 2022-.
8 	License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
9 	Authors: $(WEB Per Nordlöw)
10 
11 	See_Also: http://stackoverflow.com/questions/1902340/can-a-sha-1-hash-be-all-zeroes
12 	See_Also: http://stackoverflow.com/questions/20179287/sha1-indexed-hash-table-in-d
13 */
14 struct Digest(size_t numBytes = 20, string name = "SHA-1") {
15 	static assert(hash_t.sizeof == 4 ||
16 				  hash_t.sizeof == 8,
17 				  ("Unsupported size of hash_t " ~ to!string(hash_t.sizeof)));
18 
19 	//enum numBytes = 20;		 /+ TODO: templatize +/
20 	enum numInts = numBytes / 4;
21 	static assert(numBytes % 4 == 0, "numBytes must be a multiple of 4");
22 
23 	import std.conv: to;
24 
25 	/** Data */
26 	static	  if (hash_t.sizeof == 4) align(4) ubyte[numBytes] _bytes;
27 	else static if (hash_t.sizeof == 8) align(8) ubyte[numBytes] _bytes;
28 	else
29 	{
30 		static assert(0, "Cannot handle hash_t of size " ~ to!string(hash_t.sizeof));
31 	}
32 
33 	alias _bytes this;
34 
35 	hash_t toHash() const @property @trusted pure nothrow
36 	{
37 		static assert(hash_t.sizeof <= numBytes, "hash_t.sizeof must be <= numBytes");
38 		auto x = _bytes[0..hash_t.sizeof]; // Use offset 0 to preserv alignment of _bytes
39 		return *(cast(hash_t*)&x); // which is required for this execute
40 	}
41 
42 	// Alternatively: writefln("%-(%02x%)", digest[0 .. numBytes])
43 	string toString(bool abbreviated = true) const @property @trusted pure /* nothrow */
44 	{
45 		import std.digest.sha: toHexString;
46 		import std.range: chunks;
47 		import std.algorithm: map, joiner;
48 		if (abbreviated)
49 			return name ~ ":" ~ _bytes[0..4].toHexString ~ "\u2026";
50 		else
51 			return name ~ ":" ~ _bytes[].chunks(4).map!toHexString.joiner(":").to!string;
52 	}
53 
54 	/** Check if digest is undefined. */
55 	bool empty() @property @safe const pure nothrow
56 	{
57 		/* TODO: Is this unrolled to uint/ulong compares? */
58 		import nxt.predicates: allZero;
59 		return (cast(uint[numInts])_bytes)[].allZero; // Note: D understands that this is @safe! :)
60 	}
61 
62 	/** Check if digest is defined. */
63 	bool defined() @property @safe const pure nothrow => !empty;
64 
65 	/** Check if digest is initialized. */
66 	bool opCast(T : bool)() const @safe pure nothrow => defined;
67 
68 	version (none) {
69 		Digest opBinary(string op)(Digest rhs) {
70 			typeof(return) tmp = void;
71 			static if (op == "^" ||
72 					   op == "|" ||
73 					   op == "&")
74 				mixin("tmp = _bytes[] " ~ op ~ " rhs._bytes[];");
75 			else
76 				static assert(0, "Unsupported binary operator " ~ op);
77 			return tmp;
78 		}
79 	}
80 
81 	void toMsgpack(Packer)(ref Packer packer) const
82 	{
83 		immutable bool definedFlag = defined;
84 		packer.pack(definedFlag);
85 		if (definedFlag) {
86 			packer.pack(_bytes); // no header
87 		}
88 	}
89 
90 	void fromMsgpack(Unpacker)(auto ref Unpacker unpacker) {
91 		bool definedFlag = void;
92 		unpacker.unpack(definedFlag);
93 		if (definedFlag) {
94 			unpacker.unpack(_bytes); // no header
95 		}
96 		else
97 		{
98 			_bytes[] = 0; // zero it!
99 		}
100 	}
101 }
102 
103 alias MD5Digest	= Digest!(16, "MD5");
104 alias SHA1Digest   = Digest!(20, "SHA-1");
105 alias SHA224Digest = Digest!(28, "SHA-224");
106 alias SHA256Digest = Digest!(32, "SHA-256");
107 alias SHA384Digest = Digest!(48, "SHA-384");
108 alias SHA512Digest = Digest!(64, "SHA-512");
109 
110 pure nothrow @safe @nogc unittest {
111 	SHA1Digest a, b;
112 	assert(a.empty);
113 	assert(b.empty);
114 	assert(a == b);
115 	/* a[0] = 1; */
116 	/* b[0] = 1; */
117 	/* b[1] = 1; */
118 	/* const c = a^b; */
119 	/* assert(c[0] == 0); */
120 	/* assert(c[1] == 1); */
121 	/* assert(!c.empty); */
122 }