Skip to content

Commit 8e2e8d5

Browse files
committed
Improve documentation, usability and tests
This commit introduce a lot more documentation (README, inline) in preparation for Bitblob's release. On the functional side, it also adds an opSlice overload, make the existing opIndex overload inout, remove the dependency on std.format, and add tests that we indeed do not allocate when using formattedWrite.
1 parent 4a7f812 commit 8e2e8d5

File tree

2 files changed

+148
-20
lines changed

2 files changed

+148
-20
lines changed

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,55 @@
11
# bitblob
2-
A small value type to represent hashes
2+
3+
A lightweight wrapper to represent hashes as value types.
4+
5+
## Example
6+
7+
```D
8+
/// Alias for a 256 bits / 32 byte hash type
9+
alias Hash = BitBlob!256;
10+
11+
/// Used in the following tests
12+
enum BTCGenesisStr = `0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f`;
13+
14+
// Most functions are anotated as `nothrow`/`@nogc`/`pure`/`@safe`,
15+
// except for the two `toString` overloads
16+
@nogc @safe pure nothrow unittest
17+
{
18+
import std.digest.sha;
19+
// Can be initialized from an `ubyte[32]`
20+
// (or `ubyte[]` of length 32)
21+
const Hash fromSha = sha256Of("Hello World");
22+
23+
// Or from a string
24+
const Hash genesis = BTCGenesisStr;
25+
26+
assert(!genesis.isNull());
27+
assert(Hash.init.isNull());
28+
29+
ubyte[5] empty;
30+
assert(Hash.init < genesis);
31+
assert(genesis[0 .. 5] == empty);
32+
}
33+
34+
// This cannot be nothrow/@nogc because format is not allocated as such
35+
// However, Bitblob has tests that it does not allocate
36+
unittest
37+
{
38+
// This does not allocate
39+
char[Hash.StringBufferSize] buff;
40+
const Hash genesis = BTCGenesisStr;
41+
formattedWrite(buff[], "%s", genesis);
42+
assert(buff[] == BTCGenesisStr);
43+
}
44+
```
45+
46+
## Note on design
47+
48+
`BitBlob` is intended for convenient representation
49+
of fully computed / finalized hashes.
50+
51+
String representation, comparison, and seamless conversion
52+
from string or binary representation are the focus.
53+
54+
Byte-level manipulation of the underlying data is not
55+
prefered, although possible through `opSlice`.

source/geod24/bitblob.d

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,32 @@ module geod24.bitblob;
1818
static import std.ascii;
1919
import std.algorithm;
2020
import std.range;
21-
import std.format;
2221
import std.utf;
2322

2423
///
2524
@nogc @safe pure nothrow unittest
2625
{
27-
import std.digest.sha;
26+
/// Alias for a 256 bits / 32 byte hash type
2827
alias Hash = BitBlob!256;
29-
Hash k1 = sha256Of("Hello World");
28+
29+
import std.digest.sha;
30+
// Can be initialized from an `ubyte[32]`
31+
// (or `ubyte[]` of length 32)
32+
Hash fromSha = sha256Of("Hello World");
33+
34+
// Of from a string
35+
Hash genesis = GenesisBlockHashStr;
36+
37+
assert(!genesis.isNull());
38+
assert(Hash.init.isNull());
39+
40+
ubyte[5] empty;
41+
assert(Hash.init < genesis);
42+
// The underlying 32 bytes can be access through `opIndex` and `opSlice`
43+
assert(genesis[$ - 5 .. $] == empty);
3044
}
3145

46+
3247
/*******************************************************************************
3348
3449
A value type representing a hash
@@ -37,28 +52,56 @@ import std.utf;
3752
Bits = The size of the hash, in bits. Must be a multiple of 8.
3853
3954
*******************************************************************************/
55+
4056
public struct BitBlob (size_t Bits)
4157
{
42-
/// Used by std.format
43-
/// Cannot be `nothrow @nogc` since sformat is not, but does not allocate
44-
public void toString (scope void delegate(const(char)[]) @safe sink) const @safe
58+
@safe:
59+
60+
static assert (
61+
Bits % 8 == 0,
62+
"Argument to BitBlob must be a multiple of 8");
63+
64+
/// Convenience enum
65+
public enum StringBufferSize = (Width * 2 + 2);
66+
67+
/***************************************************************************
68+
69+
Format the hash as a lowercase hex string
70+
71+
Used by `std.format`.
72+
Does not allocate/throw if the sink does not allocate/throw.
73+
74+
***************************************************************************/
75+
76+
public void toString (scope void delegate(const(char)[]) @safe sink) const
4577
{
78+
/// Used for formatting
79+
static immutable LHexDigits = `0123456789abcdef`;
80+
4681
sink("0x");
4782
char[2] data;
4883
// retro because the data is stored in little endian
4984
this.data[].retro.each!(
5085
(bin)
5186
{
52-
sformat(data, "%0.2x", bin);
87+
data[0] = LHexDigits[bin >> 4];
88+
data[1] = LHexDigits[(bin & 0b0000_1111)];
5389
sink(data);
5490
});
5591
}
5692

57-
/// Used for serialization
58-
public string toString () const @safe
93+
/***************************************************************************
94+
95+
Get the string representation of this hash
96+
97+
Only performs one allocation.
98+
99+
***************************************************************************/
100+
101+
public string toString () const
59102
{
60103
size_t idx;
61-
char[Width * 2 + 2] buffer = void;
104+
char[StringBufferSize] buffer = void;
62105
scope sink = (const(char)[] v) {
63106
buffer[idx .. idx + v.length] = v;
64107
idx += v.length;
@@ -67,7 +110,7 @@ public struct BitBlob (size_t Bits)
67110
return buffer.idup;
68111
}
69112

70-
pure nothrow @nogc @safe:
113+
pure nothrow @nogc:
71114

72115
/***************************************************************************
73116
@@ -117,16 +160,22 @@ public struct BitBlob (size_t Bits)
117160
this.data[idx] = cast(ubyte)((chunk[0] << 4) + chunk[1]);
118161
}
119162

120-
/// Used for deserialization
121-
static auto fromString (const(char)[] str)
163+
/***************************************************************************
164+
165+
Support deserialization
166+
167+
Vibe.d expects the `toString`/`fromString` to be present for it to
168+
correctly serialize and deserialize a type.
169+
This allows to use this type as parameter in `vibe.web.rest` methods,
170+
or use it with Vibe.d's serialization module.
171+
172+
***************************************************************************/
173+
174+
static auto fromString (scope const(char)[] str)
122175
{
123176
return BitBlob!(Bits)(str);
124177
}
125178

126-
static assert (
127-
Bits % 8 == 0,
128-
"Argument to BitBlob must be a multiple of 8");
129-
130179
/// The width of this aggregate, in octets
131180
public static immutable Width = Bits / 8;
132181

@@ -136,15 +185,24 @@ public struct BitBlob (size_t Bits)
136185
/// Returns: If this BitBlob has any value
137186
public bool isNull () const
138187
{
139-
return this.data[].all!((v) => v == 0);
188+
return this == typeof(this).init;
140189
}
141190

142191
/// Used for sha256Of
143-
public const(ubyte)[] opIndex () const
192+
public inout(ubyte)[] opIndex () inout
144193
{
145194
return this.data;
146195
}
147196

197+
/// Convenience overload
198+
public inout(ubyte)[] opSlice (size_t from, size_t to) inout
199+
{
200+
return this.data[from .. to];
201+
}
202+
203+
/// Ditto
204+
alias opDollar = Width;
205+
148206
/// Public because of a visibility bug
149207
public static ubyte fromHex (char c)
150208
{
@@ -195,6 +253,23 @@ unittest
195253
alias Hash = BitBlob!256;
196254
Hash gen1 = GenesisBlockHashStr;
197255
assert(format("%s", gen1) == GenesisBlockHashStr);
256+
assert(gen1.toString() == GenesisBlockHashStr);
257+
}
258+
259+
/// Make sure `toString` does not allocate even if it's not `@nogc`
260+
unittest
261+
{
262+
import core.memory;
263+
import std.format;
264+
alias Hash = BitBlob!256;
265+
266+
Hash gen1 = GenesisBlockHashStr;
267+
char[Hash.StringBufferSize] buffer;
268+
auto statsBefore = GC.stats();
269+
formattedWrite(buffer[], "%s", gen1);
270+
auto statsAfter = GC.stats();
271+
assert(buffer == GenesisBlockHashStr);
272+
assert(statsBefore.usedSize == statsAfter.usedSize);
198273
}
199274

200275
version (unittest)

0 commit comments

Comments
 (0)