-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHashID.kt
204 lines (181 loc) · 5.59 KB
/
HashID.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package org.veupathdb.lib.hash_id
import java.io.InputStream
import java.security.MessageDigest
/**
* Hash/Digest Based Identifier.
*
* This class offers an implementation of a digest-based identifier that is more
* robust than a naked digest string. Once constructed from the source string,
* it generates the digest and efficiently stores the digest as a 128-bit
* number.
*
* This type is effectively a "new type" over `byte[16]` which provides the
* following features:
*
* * Allows consumers to make guarantees about the values they are given as
* a [HashID] can only be constructed via a valid hash either by hex string or
* by a raw array of exactly 16 bytes.
* * Eliminates the class(es) of bugs that could result from dealing with a
* builtin type, relying on ID consumers to perform validation at every
* necessary step.
* * Can be constructed via the standard constructor, or via the provided
* convenience methods allowing the [HashID] to be created from an arbitrary
* [String], [InputStream], or [Object] which will be MD5 hashed to generate
* a new `HashID` instance.
*
* This value is safe to use in [Sets][Set] and [Maps][Map].
*
* @author Elizabeth Harper
* @since v1.0.0
*/
class HashID {
private val rawBytes: ByteArray
/**
* The URL-safe, stringified form of this [HashID].
*
* This value will be a 32 digit hex string.
*
* **NOTE**: This value is not cached, and is calculated on every call to this
* property/getter.
*/
val string
get() = renderBytes(rawBytes)
/**
* A copy of the backing byte array for this [HashID].
*/
val bytes
get() = rawBytes.copyOf()
/**
* Constructs a new [HashID] backed by a copy of the given input byte array.
*
* The input byte array must have a length of exactly 16 bytes.
*
* @param bytes Byte array to back the new [HashID].
*
* @throws IllegalArgumentException If the input byte array does not contain
* exactly 16 bytes.
* @throws NullPointerException If the input byte array is `null`.
*/
constructor(bytes: ByteArray) {
if (bytes.size != 16)
throw IllegalArgumentException()
rawBytes = bytes.copyOf()
}
/**
* Constructs a new [HashID] backed by a byte array parsed from the given hex
* string.
*
* If the given string is not a valid base16 string, an exception will be
* thrown.
*
* If the given string is not exactly 32 characters in length, an exception
* will be thrown.
*
* @param stringValue A 32 digit hex string to back this [HashID].
*
* @throws IllegalArgumentException If the input value is not a valid hex
* string of exactly 32 characters.
* @throws NullPointerException If the input value is `null`.
*/
constructor(stringValue: String) {
if (stringValue.length != 32)
throw IllegalArgumentException()
rawBytes = parseBytes(stringValue)
}
/**
* The URL-safe, stringified form of this [HashID] optionally in uppercase.
*
* This value will be a 32 digit hex string.
*
* @param lowercase If `true` (the default value), the letter characters in
* the returned string will be lowercase. If `false`, the letter characters
* in the returned string will be uppercase.
*/
fun getString(lowercase: Boolean) {
renderBytes(rawBytes, lowercase)
}
/**
* Returns the stringified form of this [HashID].
*
* @return Stringified form of this [HashID].
*/
override fun toString() =
renderBytes(rawBytes)
override fun equals(other: Any?) =
when (other) {
is HashID -> rawBytes.contentEquals(other.rawBytes)
else -> false
}
override fun hashCode() = rawBytes.contentHashCode()
companion object {
/**
* Calculates the MD5 hash of the given value and wraps that hash in a new
* [HashID] instance.
*
* @param value Value that will be MD5 hashed.
*
* @return The new [HashID].
*/
@JvmStatic
fun ofMD5(value: String): HashID {
val digest = MessageDigest.getInstance("MD5")
digest.update(value.toByteArray())
return HashID(digest.digest())
}
/**
* Calculates the MD5 hash value of the contents of the given [InputStream]
* and wraps that hash in a new [HashID] instance.
*
* @param stream InputStream over the value(s) that will be MD5 hashed.
*
* @param close Whether the given input stream should be closed by this
* method.
*
* Defaults to `false`.
*
* @return The new [HashID].
*/
@JvmStatic
@JvmOverloads
fun ofMD5(stream: InputStream, close: Boolean = false): HashID {
val digest = MessageDigest.getInstance("MD5")
val stream = stream.buffered()
val buffer = ByteArray(8192)
if (close) {
stream.use {
while (true) {
val red = it.read(buffer)
digest.update(buffer, 0, red)
if (red < buffer.size) {
break
}
}
}
} else {
while (true) {
val red = stream.read(buffer)
digest.update(buffer, 0, red)
if (red < buffer.size) {
break
}
}
}
return HashID(digest.digest())
}
/**
* Calculates the MD5 hash value of the stringified form of the given value
* and wraps that hash in a new [HashID] instance.
*
* This method is a simple convenience method over:
* ```
* HashID.ofMD5(myType.toString())
* ```
*
* @param value Value that will be MD5 hashed.
*
* @return The new [HashID].
*/
@JvmStatic
fun ofMD5(value: Any) = ofMD5(value.toString())
}
}