/
digest.cr
246 lines (222 loc) · 6.87 KB
/
digest.cr
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
require "base64"
# `Digest` is the base type of hashing algorithms like `Digest::MD5`,
# `Digest::SHA1`, `Digest::SHA256`, or `Digest::SHA512`.
#
# A `Digest` instance holds the state of an ongoing hash calculation.
# It can receive new data to include in the hash via `#update`, `#<<`, or `#file`.
# Once all data is included, use `#final` or `#hexfinal` to get the hash. This will mark the
# ongoing calculation as finished. A finished calculation can't receive new data.
#
# A `digest.dup.final` call may be used to get an intermediate hash value.
#
# Use `#reset` to reuse the `Digest` instance for a new calculation.
abstract class Digest
class FinalizedError < Exception
end
# The `Digest::ClassMethods` module is used in the concrete subclass of `Digest`
# that does not require arguments in its construction.
#
# The modules adds convenient class methods as `Digest::MD5.digest`, `Digest::MD5.hexdigest`.
module ClassMethods
# Returns the hash of *data*. *data* must respond to `#to_slice`.
def digest(data) : Bytes
digest do |ctx|
ctx.update(data.to_slice)
end
end
# Yields an instance of `self` which can receive calls to `#update(data : String | Bytes)`
# and returns the finalized digest afterwards.
#
# ```
# require "digest/md5"
#
# digest = Digest::MD5.digest do |ctx|
# ctx.update "f"
# ctx.update "oo"
# end
# digest.hexfinal # => "acbd18db4cc2f85cedef654fccc4a4d8"
# ```
def digest(& : self ->) : Bytes
context = new
yield context
context.final
end
# Returns the hexadecimal representation of the hash of *data*.
#
# ```
# require "digest/md5"
#
# Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8"
# ```
def hexdigest(data) : String
hexdigest &.update(data)
end
# Yields a context object with an `#update(data : String | Bytes)`
# method available. Returns the resulting digest in hexadecimal representation
# afterwards.
#
# ```
# require "digest/md5"
#
# Digest::MD5.hexdigest("foo") # => "acbd18db4cc2f85cedef654fccc4a4d8"
# Digest::MD5.hexdigest do |ctx|
# ctx.update "f"
# ctx.update "oo"
# end
# # => "acbd18db4cc2f85cedef654fccc4a4d8"
# ```
def hexdigest(& : self ->) : String
hashsum = digest do |ctx|
yield ctx
end
hashsum.to_slice.hexstring
end
# Returns the base64-encoded hash of *data*.
#
# ```
# require "digest/sha1"
#
# Digest::SHA1.base64digest("foo") # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM="
# ```
def base64digest(data) : String
base64digest &.update(data)
end
# Yields a context object with an `#update(data : String | Bytes)`
# method available. Returns the resulting digest in base64 representation
# afterwards.
#
# ```
# require "digest/sha1"
#
# Digest::SHA1.base64digest do |ctx|
# ctx.update "f"
# ctx.update "oo"
# end
# # => "C+7Hteo/D9vJXQ3UfzxbwnXaijM="
# ```
def base64digest(& : self -> _) : String
hashsum = digest do |ctx|
yield ctx
end
Base64.strict_encode(hashsum)
end
end
@finished = false
def update(data) : self
update data.to_slice
end
def update(data : Bytes) : self
check_finished
update_impl data
self
end
# Returns the final digest output.
#
# `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls.
#
# NOTE: `.dup.final` call may be used to get an intermediate hash value.
def final : Bytes
dst = Bytes.new digest_size
final dst
end
# Puts the final digest output into `dst`.
#
# Faster than the `Bytes` allocating version.
# Use when hashing in loops.
#
# `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls.
#
# NOTE: `.dup.final(dst)` call may be used to get an intermediate hash value.
def final(dst : Bytes) : Bytes
check_finished
@finished = true
final_impl dst
dst
end
# Returns a hexadecimal-encoded digest in a new `String`.
#
# `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls.
#
# NOTE: `.dup.hexfinal` call may be used to get an intermediate hash value.
def hexfinal : String
dsize = digest_size
string_size = dsize * 2
String.new(string_size) do |buffer|
tmp = Slice.new(buffer + dsize, dsize)
final tmp
tmp.hexstring buffer
{string_size, string_size}
end
end
# Writes a hexadecimal-encoded digest to `dst`.
#
# Faster than the `String` allocating version.
# Use when hashing in loops.
#
# `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls.
#
# NOTE: `.dup.final` call may be used to get an intermediate hash value.
def hexfinal(dst : Bytes) : Nil
dsize = digest_size
unless dst.bytesize == dsize * 2
raise ArgumentError.new("Incorrect dst size: #{dst.bytesize}, expected: #{dsize * 2}")
end
tmp = dst[dsize, dsize]
final tmp
tmp.hexstring dst
end
# Writes a hexadecimal-encoded digest to `IO`.
#
# Faster than the `String` allocating version.
#
# `final` or `hexfinal` can only be called once and raises `FinalizedError` on subsequent calls.
#
# NOTE: `.dup.final` call may be used to get an intermediate hash value.
#
# This method is restricted to a maximum digest size of 64 bits. Implementations that allow
# a larger digest size should override this method to use a larger buffer.
def hexfinal(io : IO) : Nil
if digest_size > 64
raise "Digest#hexfinal(IO) can't handle digest_size over 64 bits"
end
sary = uninitialized StaticArray(UInt8, 128)
tmp = sary.to_slice[0, digest_size * 2]
hexfinal tmp
io << tmp
end
# Resets the state of this object. Use to get another hash value without creating a new object.
def reset : self
reset_impl
@finished = false
self
end
# Reads the file's content and updates the digest with it.
def file(file_name : Path | String) : self
File.open(file_name) do |io|
self << io
end
end
# Reads the io's data and updates the digest with it.
def update(io : IO) : self
buffer = uninitialized UInt8[4096]
while (read_bytes = io.read(buffer.to_slice)) > 0
self << buffer.to_slice[0, read_bytes]
end
self
end
# :ditto:
def <<(data) : self
update(data)
end
private def check_finished : Nil
raise FinalizedError.new("finish already called") if @finished
end
# Hashes data incrementally.
abstract def update_impl(data : Bytes) : Nil
# Stores the output digest of #digest_size bytes in dst.
abstract def final_impl(dst : Bytes) : Nil
# Resets the object to it's initial state.
abstract def reset_impl : Nil
# Returns the digest output size in bytes.
abstract def digest_size : Int32
end