gel 0.3.0 → 0.8.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -3
- data/RELEASING.md +12 -0
- data/exe/gel +4 -2
- data/gemlib/gel/stub.rb +20 -0
- data/lib/gel/catalog/common.rb +4 -2
- data/lib/gel/catalog/compact_index.rb +6 -10
- data/lib/gel/catalog/dependency_index.rb +10 -10
- data/lib/gel/catalog/legacy_index.rb +4 -6
- data/lib/gel/catalog/marshal_hacks.rb +2 -0
- data/lib/gel/catalog.rb +33 -52
- data/lib/gel/catalog_set.rb +100 -0
- data/lib/gel/command/help.rb +13 -2
- data/lib/gel/command/lock.rb +3 -3
- data/lib/gel/command/open.rb +24 -0
- data/lib/gel/command/shell_setup.rb +11 -8
- data/lib/gel/command/stub.rb +45 -2
- data/lib/gel/command/version.rb +7 -0
- data/lib/gel/command.rb +43 -6
- data/lib/gel/compatibility/rubygems.rb +10 -197
- data/lib/gel/compatibility.rb +2 -2
- data/lib/gel/config.rb +41 -7
- data/lib/gel/db.rb +93 -83
- data/lib/gel/direct_gem.rb +16 -4
- data/lib/gel/environment.rb +542 -249
- data/lib/gel/error.rb +156 -24
- data/lib/gel/gemfile_parser.rb +74 -12
- data/lib/gel/gemspec_parser.rb +26 -7
- data/lib/gel/git_catalog.rb +15 -3
- data/lib/gel/git_depot.rb +62 -28
- data/lib/gel/httpool.rb +5 -2
- data/lib/gel/installer.rb +61 -23
- data/lib/gel/lock_loader.rb +87 -112
- data/lib/gel/lock_parser.rb +23 -31
- data/lib/gel/locked_store.rb +30 -21
- data/lib/gel/multi_store.rb +13 -4
- data/lib/gel/null_solver.rb +67 -0
- data/lib/gel/package/abortable.rb +18 -0
- data/lib/gel/package/installer.rb +124 -49
- data/lib/gel/package.rb +21 -4
- data/lib/gel/path_catalog.rb +1 -1
- data/lib/gel/pinboard.rb +4 -2
- data/lib/gel/platform.rb +38 -0
- data/lib/gel/pub_grub/package.rb +67 -0
- data/lib/gel/pub_grub/preference_strategy.rb +10 -6
- data/lib/gel/pub_grub/solver.rb +37 -0
- data/lib/gel/pub_grub/source.rb +64 -92
- data/lib/gel/resolved_gem_set.rb +234 -0
- data/lib/gel/runtime.rb +3 -3
- data/lib/gel/set.rb +62 -0
- data/lib/gel/stdlib.rb +83 -0
- data/lib/gel/store.rb +94 -25
- data/lib/gel/store_catalog.rb +2 -2
- data/lib/gel/store_gem.rb +54 -6
- data/lib/gel/stub_set.rb +32 -2
- data/lib/gel/support/cgi_escape.rb +34 -0
- data/lib/gel/support/gem_platform.rb +0 -2
- data/lib/gel/support/sha512.rb +142 -0
- data/lib/gel/support/tar/tar_writer.rb +2 -2
- data/lib/gel/tail_file.rb +2 -1
- data/lib/gel/util.rb +108 -0
- data/lib/gel/vendor/pstore.rb +3 -0
- data/lib/gel/vendor/pub_grub.rb +3 -0
- data/lib/gel/vendor/ruby_digest.rb +3 -0
- data/lib/gel/vendor_catalog.rb +38 -0
- data/lib/gel/version.rb +1 -1
- data/lib/gel.rb +15 -0
- data/man/man1/gel-exec.1 +1 -1
- data/man/man1/gel-install.1 +1 -1
- data/man/man1/gel.1 +14 -1
- data/{lib/gel/compatibility → slib}/bundler/cli.rb +0 -0
- data/{lib/gel/compatibility → slib}/bundler/friendly_errors.rb +0 -0
- data/{lib/gel/compatibility/rubygems/dependency_installer.rb → slib/bundler/gem_helper.rb} +0 -0
- data/slib/bundler/gem_tasks.rb +0 -0
- data/{lib/gel/compatibility → slib}/bundler/setup.rb +0 -0
- data/{lib/gel/compatibility → slib}/bundler.rb +39 -3
- data/{lib/gel/compatibility → slib}/rubygems/command.rb +0 -0
- data/slib/rubygems/dependency_installer.rb +12 -0
- data/{lib/gel/compatibility → slib}/rubygems/gem_runner.rb +0 -0
- data/slib/rubygems/package.rb +6 -0
- data/slib/rubygems/package_task.rb +7 -0
- data/slib/rubygems/specification.rb +0 -0
- data/slib/rubygems/version.rb +0 -0
- data/slib/rubygems.rb +297 -0
- data/vendor/pstore/LICENSE.txt +22 -0
- data/vendor/pstore/lib/pstore.rb +488 -0
- data/vendor/pub_grub/LICENSE.txt +21 -0
- data/vendor/pub_grub/lib/pub_grub/assignment.rb +20 -0
- data/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +183 -0
- data/vendor/pub_grub/lib/pub_grub/failure_writer.rb +182 -0
- data/vendor/pub_grub/lib/pub_grub/incompatibility.rb +143 -0
- data/vendor/pub_grub/lib/pub_grub/package.rb +35 -0
- data/vendor/pub_grub/lib/pub_grub/partial_solution.rb +121 -0
- data/vendor/pub_grub/lib/pub_grub/rubygems.rb +45 -0
- data/vendor/pub_grub/lib/pub_grub/solve_failure.rb +17 -0
- data/vendor/pub_grub/lib/pub_grub/static_package_source.rb +53 -0
- data/vendor/pub_grub/lib/pub_grub/term.rb +105 -0
- data/vendor/pub_grub/lib/pub_grub/version.rb +3 -0
- data/vendor/pub_grub/lib/pub_grub/version_constraint.rb +124 -0
- data/vendor/pub_grub/lib/pub_grub/version_range.rb +399 -0
- data/vendor/pub_grub/lib/pub_grub/version_solver.rb +247 -0
- data/vendor/pub_grub/lib/pub_grub/version_union.rb +174 -0
- data/vendor/pub_grub/lib/pub_grub.rb +31 -0
- data/vendor/ruby-digest/UNLICENSE +24 -0
- data/vendor/ruby-digest/lib/ruby_digest.rb +812 -0
- metadata +95 -19
@@ -0,0 +1,812 @@
|
|
1
|
+
#--
|
2
|
+
# Ruby Digest
|
3
|
+
# =============================================================================
|
4
|
+
# [![Status](https://travis-ci.org/Solistra/ruby-digest.svg?branch=master)][ci]
|
5
|
+
# [ci]: https://travis-ci.org/Solistra/ruby-digest
|
6
|
+
#
|
7
|
+
# Summary
|
8
|
+
# -----------------------------------------------------------------------------
|
9
|
+
# Ruby Digest aims to provide pure-Ruby implementations of the digest objects
|
10
|
+
# provided by the MRI Ruby 'digest' standard library (originally written as
|
11
|
+
# native C extensions). At present, Ruby Digest accurately implements the
|
12
|
+
# `MD5`, `SHA1`, and `SHA256` hashing algorithms, the Bubble Babble encoding,
|
13
|
+
# and the `HMAC` keyed-hash message authentication code.
|
14
|
+
#
|
15
|
+
# Ruby Digest has been provided primarily for Ruby environments which do not
|
16
|
+
# have access to native extensions for any reason (notable examples include
|
17
|
+
# the RPG Maker series and SketchUp Make).
|
18
|
+
#
|
19
|
+
# Notes
|
20
|
+
# -----------------------------------------------------------------------------
|
21
|
+
# While Ruby Digest aims to provide a reasonable, pure-Ruby alternative to
|
22
|
+
# the MRI Ruby 'digest' standard library, there are a few notable classes
|
23
|
+
# missing -- namely the `RMD160`, `SHA384`, and `SHA512` classes.
|
24
|
+
#
|
25
|
+
# License
|
26
|
+
# -----------------------------------------------------------------------------
|
27
|
+
# Ruby Digest is free and unencumbered software released into the public
|
28
|
+
# domain.
|
29
|
+
#
|
30
|
+
#++
|
31
|
+
|
32
|
+
# Gel::Vendor::RubyDigest
|
33
|
+
# =============================================================================
|
34
|
+
# Provides a pure-Ruby implementation of the MRI 'digest' standard library.
|
35
|
+
module Gel::Vendor::RubyDigest
|
36
|
+
# The semantic version of {Gel::Vendor::RubyDigest}.
|
37
|
+
VERSION = '0.0.1pre'.freeze
|
38
|
+
|
39
|
+
# A specifically-ordered array of lower-case vowels used to create Bubble
|
40
|
+
# Babble-encoded digest hash values.
|
41
|
+
VOWELS = %w( a e i o u y ).freeze
|
42
|
+
|
43
|
+
# A specifically-ordered array of lower-case consonants used to create
|
44
|
+
# Bubble Babble-encoded digest hash values.
|
45
|
+
CONSONANTS = %w( b c d f g h k l m n p r s t v z x ).freeze
|
46
|
+
|
47
|
+
# Encodes the given string in Bubble Babble (an encoding designed to be more
|
48
|
+
# human-readable than hexadecimal).
|
49
|
+
#
|
50
|
+
# @param string [String] the string to encode in Bubble Babble
|
51
|
+
# @return [String] the Bubble Babble-encoded string
|
52
|
+
# @see http://wiki.yak.net/589/Bubble_Babble_Encoding.txt
|
53
|
+
def self.bubblebabble(string)
|
54
|
+
d = string
|
55
|
+
seed = 1
|
56
|
+
babble = 'x'
|
57
|
+
length = d.length
|
58
|
+
rounds = (length / 2) + 1
|
59
|
+
0.upto(rounds - 1) do |i|
|
60
|
+
if i + 1 < (rounds || length % 2)
|
61
|
+
i0 = (((d[2 * i].ord >> 6) & 3) + seed) % 6
|
62
|
+
i1 = (d[2 * i].ord >> 2) & 15
|
63
|
+
i2 = ((d[2 * i].ord & 3) + seed / 6) % 6
|
64
|
+
babble << "#{VOWELS[i0]}#{CONSONANTS[i1]}#{VOWELS[i2]}"
|
65
|
+
if (i + 1 < rounds)
|
66
|
+
i0 = (d[2 * i + 1].ord >> 4) & 15
|
67
|
+
i1 = d[2 * i + 1].ord & 15
|
68
|
+
babble << "#{CONSONANTS[i0]}-#{CONSONANTS[i1]}"
|
69
|
+
seed = ((seed * 5) + (d[2 * i].ord * 7) + d[2 * i + 1].ord) % 36
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if length.even?
|
73
|
+
babble << "#{VOWELS[seed % 6]}#{CONSONANTS[16]}#{VOWELS[seed / 6]}"
|
74
|
+
else
|
75
|
+
i0 = (((d[length - 1].ord >> 6) & 3) + seed) % 6
|
76
|
+
i1 = (d[length - 1].ord >> 2) & 15
|
77
|
+
i2 = (((d[length - 1].ord) & 3) + seed / 6) % 6
|
78
|
+
babble << "#{VOWELS[i0]}#{CONSONANTS[i1]}#{VOWELS[i2]}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
babble << 'x'
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generates a hex-encoded version of the given `string`.
|
86
|
+
#
|
87
|
+
# @param string [String] the string to hex-encode
|
88
|
+
# @return [String] the hex-encoded string
|
89
|
+
def self.hexencode(string)
|
90
|
+
string.unpack('H*').pack('A*')
|
91
|
+
end
|
92
|
+
|
93
|
+
# Instance
|
94
|
+
# ===========================================================================
|
95
|
+
# Provides instance methods for a digest implementation object to calculate
|
96
|
+
# message digest values.
|
97
|
+
module Instance
|
98
|
+
# If a string is given, checks whether or not it is equal to the hex-
|
99
|
+
# encoded hash value of this digest object. If another digest instance is
|
100
|
+
# given, checks whether or not they have the same hexadecimal hash value.
|
101
|
+
#
|
102
|
+
# @param other [Object] the other object to compare this digest to
|
103
|
+
# @return [Boolean] `true` if both objects have matching digest values,
|
104
|
+
# `false` otherwise
|
105
|
+
def ==(other)
|
106
|
+
hexdigest == (other.respond_to?(:hexdigest) ? other.hexdigest : other)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @raise [RuntimeError] if a subclass does not implement this method
|
110
|
+
def block_length
|
111
|
+
raise RuntimeError, "#{self.class} does not implement block_length()"
|
112
|
+
end
|
113
|
+
|
114
|
+
# @raise [RuntimeError] if a subclass does not implement this method
|
115
|
+
def digest_length
|
116
|
+
raise RuntimeError, "#{self.class} does not implement digest_length()"
|
117
|
+
end
|
118
|
+
alias_method :length, :digest_length
|
119
|
+
alias_method :size, :length
|
120
|
+
|
121
|
+
# Returns the resulting base64-encoded hash value of the digest if no
|
122
|
+
# string is given, maintaining the digest's state. If a string is given,
|
123
|
+
# returns the base64-encoded hash value of the given string, resetting the
|
124
|
+
# digest to its initial state.
|
125
|
+
#
|
126
|
+
# @param str [String, nil] the string to produce a base64-encoded hash
|
127
|
+
# value for if given
|
128
|
+
# @return [String] the requested base64-encoded digest
|
129
|
+
def base64digest(str = nil)
|
130
|
+
str.nil? ? clone.base64digest! :
|
131
|
+
new.update(str).base64digest!.tap { reset }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the resulting hash value in a base64-encoded form and resets the
|
135
|
+
# digest to its initial state.
|
136
|
+
#
|
137
|
+
# @return [String] the base64-encoded hash value of this digest object
|
138
|
+
# before the reset
|
139
|
+
def base64digest!
|
140
|
+
[finish].pack('m0').tap { reset }
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [String] the Bubble Babble-encoded hash value of this digest
|
144
|
+
# object
|
145
|
+
# @see Gel::Vendor::RubyDigest.bubblebabble
|
146
|
+
def bubblebabble
|
147
|
+
Gel::Vendor::RubyDigest.bubblebabble(digest)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the resulting hash value of the digest if no string is given,
|
151
|
+
# maintaining the digest's state. If a string is given, returns the hash
|
152
|
+
# value of the given string, resetting the digest to its initial state.
|
153
|
+
#
|
154
|
+
# @param str [String, nil] the string to produce a hash value for if given
|
155
|
+
# @return [String] the requested digest
|
156
|
+
def digest(str = nil)
|
157
|
+
str.nil? ? clone.digest! : new.update(str).digest!.tap { reset }
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns the resulting hash value and resets the digest object to its
|
161
|
+
# initial state.
|
162
|
+
#
|
163
|
+
# @return [String] the hash value of this digest object before the reset
|
164
|
+
def digest!
|
165
|
+
finish.tap { reset }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Updates this digest with the contents of the given `filename` and returns
|
169
|
+
# the updated digest object.
|
170
|
+
#
|
171
|
+
# @param filename [String] the path to a file used to update the digest
|
172
|
+
# @return [self] the updated digest instance
|
173
|
+
def file(filename)
|
174
|
+
File.open(filename, 'rb') do |file|
|
175
|
+
buffer = ''
|
176
|
+
update(buffer) while file.read(16384, buffer)
|
177
|
+
end
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
# @raise [RuntimeError] if a subclass does not implement this method
|
182
|
+
def finish
|
183
|
+
raise RuntimeError, "#{self.class} does not implement finish()"
|
184
|
+
end
|
185
|
+
private :finish
|
186
|
+
|
187
|
+
# Returns the resulting hex-encoded hash value of the digest if no string
|
188
|
+
# is given, maintaining the digest's state. If a string is given, returns
|
189
|
+
# the hex-encoded hash value of the given string, resetting the digest to
|
190
|
+
# its initial state.
|
191
|
+
#
|
192
|
+
# @param str [String, nil] the string to produce a hex-encoded hash value
|
193
|
+
# for if given
|
194
|
+
# @return [String] the requested hex-encoded digest
|
195
|
+
def hexdigest(str = nil)
|
196
|
+
str.nil? ? clone.hexdigest! : new.update(str).hexdigest!.tap { reset }
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns the resulting hash value in a hex-encoded form and resets the
|
200
|
+
# digest object to its initial state.
|
201
|
+
#
|
202
|
+
# @return [String] the hex-encoded hash value of this digest object before
|
203
|
+
# the reset
|
204
|
+
def hexdigest!
|
205
|
+
finish.unpack('H*').pack('A*').tap { reset }
|
206
|
+
end
|
207
|
+
|
208
|
+
# @return [Gel::Vendor::RubyDigest::Class] a new, initialized copy of this digest object
|
209
|
+
def new
|
210
|
+
clone.reset
|
211
|
+
end
|
212
|
+
|
213
|
+
# @raise [RuntimeError] if a subclass does not implement this method
|
214
|
+
def reset
|
215
|
+
raise RuntimeError, "#{self.class} does not implement reset()"
|
216
|
+
end
|
217
|
+
|
218
|
+
# @return [String] the hex-encoded hash value of this digest object
|
219
|
+
def to_s
|
220
|
+
hexdigest
|
221
|
+
end
|
222
|
+
|
223
|
+
# @raise [RuntimeError] if a subclass does not implement this method
|
224
|
+
def update(string)
|
225
|
+
raise RuntimeError, "#{self.class} does not implement update()"
|
226
|
+
end
|
227
|
+
alias_method :<<, :update
|
228
|
+
end
|
229
|
+
# Class
|
230
|
+
# ===========================================================================
|
231
|
+
# Stands as a base class for digest implementation classes.
|
232
|
+
class Class
|
233
|
+
include Instance
|
234
|
+
|
235
|
+
# The 8-bit field used for bitwise `AND` masking. Defaults to `0xFFFFFFFF`.
|
236
|
+
MASK = 0xFFFFFFFF
|
237
|
+
|
238
|
+
# Hashes the given string, returning the base64-encoded digest.
|
239
|
+
#
|
240
|
+
# @param string [String] the string to generate a base64-encoded digest for
|
241
|
+
# @return [String] the base64-encoded digest of the given string
|
242
|
+
def self.base64digest(string, *arguments)
|
243
|
+
new(*arguments).update(string).base64digest!
|
244
|
+
end
|
245
|
+
|
246
|
+
# Hashes the given string, returning the Bubble Babble-encoded digest.
|
247
|
+
#
|
248
|
+
# @param string [String] the string generate a Bubble Babble-encoded digest
|
249
|
+
# for
|
250
|
+
# @return [String] the Bubble Babble-encoded digest of the given string
|
251
|
+
# @see Gel::Vendor::RubyDigest.bubblebabble
|
252
|
+
def self.bubblebabble(string)
|
253
|
+
Gel::Vendor::RubyDigest.bubblebabble(digest(string))
|
254
|
+
end
|
255
|
+
|
256
|
+
# Hashes the given string, returning the digest.
|
257
|
+
#
|
258
|
+
# @param string [String] the string to generate a digest for
|
259
|
+
# @return [String] the digest of the given string
|
260
|
+
def self.digest(string, *arguments)
|
261
|
+
new(*arguments).update(string).digest!
|
262
|
+
end
|
263
|
+
|
264
|
+
# Hashes the given string, returning the hex-encoded digest.
|
265
|
+
#
|
266
|
+
# @param string [String] the string to generate a hex-encoded digest for
|
267
|
+
# @return [String] the hex-encoded digest of the given string
|
268
|
+
def self.hexdigest(string, *arguments)
|
269
|
+
new(*arguments).update(string).hexdigest!
|
270
|
+
end
|
271
|
+
|
272
|
+
# Generates a new digest object representing the hashed contents of the
|
273
|
+
# given file.
|
274
|
+
#
|
275
|
+
# @param filename [String] the path to a file to generate a digest object
|
276
|
+
# for
|
277
|
+
# @return [Base] a new digest object representing the hashed contents of
|
278
|
+
# the given file
|
279
|
+
def self.file(filename, *arguments)
|
280
|
+
new(*arguments).file(filename)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
# Base
|
284
|
+
# ===========================================================================
|
285
|
+
# Abstract class providing a common interface to message digest
|
286
|
+
# implementation classes.
|
287
|
+
class Base < Class
|
288
|
+
# Customizes object instantiation to raise a `NotImplementedError` if the
|
289
|
+
# object to be initialized is a {Gel::Vendor::RubyDigest::Base} object.
|
290
|
+
#
|
291
|
+
# @param args [Array<Object>] the arguments to pass to `#initialize`
|
292
|
+
# @return [Gel::Vendor::RubyDigest::Base] the new digest object instance
|
293
|
+
# @raise [NotImplementedError] if the requested digest object is exactly
|
294
|
+
# {Gel::Vendor::RubyDigest::Base}
|
295
|
+
def self.new(*args, &block)
|
296
|
+
instance = super
|
297
|
+
if instance.class == Base
|
298
|
+
raise NotImplementedError, "#{self} is an abstract class"
|
299
|
+
end
|
300
|
+
instance
|
301
|
+
end
|
302
|
+
|
303
|
+
# Defaults to the length of the {#digest} value for this digest object.
|
304
|
+
#
|
305
|
+
# @return [Fixnum] the length of the hash value of this digest object
|
306
|
+
def digest_length
|
307
|
+
digest.length
|
308
|
+
end
|
309
|
+
|
310
|
+
# Initializes a new digest object with an empty initial state.
|
311
|
+
#
|
312
|
+
# @return [self] the new digest object
|
313
|
+
def initialize
|
314
|
+
@buffer = ''
|
315
|
+
end
|
316
|
+
|
317
|
+
# Customizes duplication of this digest object, properly setting the buffer
|
318
|
+
# of the duplicate to a copy of the source object's buffer.
|
319
|
+
#
|
320
|
+
# @note This method exists so that duplicate digest objects do not refer to
|
321
|
+
# the same base string buffer as the source digest object.
|
322
|
+
#
|
323
|
+
# @param source [Gel::Vendor::RubyDigest::Base] the digest object being duplicated
|
324
|
+
# @return [Gel::Vendor::RubyDigest::Base] the duplicate digest object
|
325
|
+
def initialize_copy(source)
|
326
|
+
super
|
327
|
+
@buffer = source.instance_eval { @buffer.clone }
|
328
|
+
end
|
329
|
+
|
330
|
+
# @return [String] a human-readable representation of this digest object
|
331
|
+
def inspect
|
332
|
+
"#<#{self.class.name}: #{hexdigest}>"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Updates the digest with the given `string`, returning the updated digest
|
336
|
+
# instance.
|
337
|
+
#
|
338
|
+
# @param string [String] the string to update the digest with
|
339
|
+
# @return [self] the updated digest instance
|
340
|
+
# @raise [TypeError] if the given `string` is not a string
|
341
|
+
def update(string)
|
342
|
+
unless string.kind_of?(String)
|
343
|
+
raise TypeError, "can't convert #{string.class.inspect} into String"
|
344
|
+
end
|
345
|
+
tap { @buffer << string }
|
346
|
+
end
|
347
|
+
alias_method :<<, :update
|
348
|
+
|
349
|
+
# Resets the digest object to its initial state and returns the digest
|
350
|
+
# instance.
|
351
|
+
#
|
352
|
+
# @return [self] the reset digest instance
|
353
|
+
def reset
|
354
|
+
tap { @buffer.clear }
|
355
|
+
end
|
356
|
+
end
|
357
|
+
# MD5
|
358
|
+
# ===========================================================================
|
359
|
+
# Provides a pure-Ruby implementation of an MD5 digest object.
|
360
|
+
class MD5 < Base
|
361
|
+
# The initial constant values for the 32-bit constant words A, B, C, and D,
|
362
|
+
# respectively.
|
363
|
+
@@words = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476]
|
364
|
+
|
365
|
+
# Generate and store the initial constant values used by the MD5 algorithm
|
366
|
+
# to mutate data -- all of this information is directly used in the
|
367
|
+
# `.hexdigest` method of this class.
|
368
|
+
@@initial_values = lambda do |f, t, k, s, n4|
|
369
|
+
1.upto(64) do |i|
|
370
|
+
t[i] = (Math.sin(i).abs * 0x100000000).truncate
|
371
|
+
n4[i] = Array.new(4) { |j| ((65 - i) + j) % 4 }
|
372
|
+
case i
|
373
|
+
when 1..16
|
374
|
+
f[i] = lambda { |x, y, z| (x & y) | ((~x) & z) }
|
375
|
+
k[i] = i - 1
|
376
|
+
s[i] = [7, 12, 17, 22][(i - 1) % 4]
|
377
|
+
when 17..32
|
378
|
+
f[i] = lambda { |x, y, z| (x & z) | (y & (~z)) }
|
379
|
+
k[i] = (1 + (i - 17) * 5) % 16
|
380
|
+
s[i] = [5, 9, 14, 20][(i - 1) % 4]
|
381
|
+
when 33..48
|
382
|
+
f[i] = lambda { |x, y, z| x ^ y ^ z }
|
383
|
+
k[i] = (5 + (i - 33) * 3) % 16
|
384
|
+
s[i] = [4, 11, 16, 23][(i - 1) % 4]
|
385
|
+
when 49..64
|
386
|
+
f[i] = lambda { |x, y, z| y ^ (x | (~z)) }
|
387
|
+
k[i] = ((i - 49) * 7) % 16
|
388
|
+
s[i] = [6, 10, 15, 21][(i - 1) % 4]
|
389
|
+
end
|
390
|
+
end
|
391
|
+
[f, t, k, s, n4]
|
392
|
+
end.call(*Array.new(5) { [] })
|
393
|
+
|
394
|
+
# @return [64] MD5 digests always have a block length of 64 bytes
|
395
|
+
def block_length
|
396
|
+
64
|
397
|
+
end
|
398
|
+
|
399
|
+
# @return [16] MD5 digests always have a length of 16 bytes
|
400
|
+
def digest_length
|
401
|
+
16
|
402
|
+
end
|
403
|
+
alias_method :length, :digest_length
|
404
|
+
alias_method :size, :length
|
405
|
+
|
406
|
+
# Hashes the buffer of this MD5 digest object, returning the computed hash
|
407
|
+
# value.
|
408
|
+
#
|
409
|
+
# @return [String] the hash value of this MD5 digest object
|
410
|
+
def finish
|
411
|
+
words = @@words.dup
|
412
|
+
f, t, k, s, n4 = *@@initial_values
|
413
|
+
generate_split_buffer(@buffer) do |chunk|
|
414
|
+
words2 = words.dup
|
415
|
+
1.upto(64) do |r|
|
416
|
+
words[n4[r][0]] = MASK & (words[n4[r][0]] +
|
417
|
+
f[r].call(*n4[r][1..3].map { |e| words[e] }) + chunk[k[r]] + t[r])
|
418
|
+
words[n4[r][0]] = rotate(words[n4[r][0]], s[r])
|
419
|
+
words[n4[r][0]] = MASK & (words[n4[r][0]] + words[n4[r][1]])
|
420
|
+
end
|
421
|
+
words.map!.with_index { |word, index| MASK & (word + words2[index]) }
|
422
|
+
end
|
423
|
+
words.reduce('') do |digest, word|
|
424
|
+
digest << [MASK & word].pack('V')
|
425
|
+
end.tap { reset }
|
426
|
+
end
|
427
|
+
private :finish
|
428
|
+
|
429
|
+
# Generates a split buffer of string values used to perform the main loop
|
430
|
+
# of the hashing algorithm.
|
431
|
+
#
|
432
|
+
# @param string [String] the base string to generate a split buffer from
|
433
|
+
# @yieldreturn [String] each chunk of the split buffer
|
434
|
+
# @return [Array<String>] the split buffer
|
435
|
+
def generate_split_buffer(string)
|
436
|
+
size = string.size * 8
|
437
|
+
buffer = string + ['10000000'].pack('B8')
|
438
|
+
buffer << [0].pack('C') while buffer.size % 64 != 56
|
439
|
+
buffer << [MASK & size].pack('V') + [size >> 32].pack('V')
|
440
|
+
split = [].tap do |a|
|
441
|
+
(buffer.size / 64).times { |i| a[i] = buffer[i*64,64].unpack('V16') }
|
442
|
+
end
|
443
|
+
block_given? ? split.each { |chunk| yield chunk } : split
|
444
|
+
end
|
445
|
+
private :generate_split_buffer
|
446
|
+
|
447
|
+
# Binary left-rotates the given `value` by the given number of `spaces`.
|
448
|
+
#
|
449
|
+
# @param value [Fixnum] the value to binary left-rotate
|
450
|
+
# @param spaces [Fixnum] the number of spaces to shift to the left
|
451
|
+
# @return [Fixnum] the left-rotated value
|
452
|
+
def rotate(value, spaces)
|
453
|
+
value << spaces | value >> (32 - spaces)
|
454
|
+
end
|
455
|
+
private :rotate
|
456
|
+
end
|
457
|
+
# SHA1
|
458
|
+
# ===========================================================================
|
459
|
+
# Provides a pure-Ruby implementation of an SHA1 digest object.
|
460
|
+
class SHA1 < Base
|
461
|
+
# The initial constant values for the 32-bit constant words A, B, C, D, and
|
462
|
+
# E, respectively.
|
463
|
+
@@words = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
|
464
|
+
|
465
|
+
# @return [64] SHA1 digests always have a block length of 64 bytes
|
466
|
+
def block_length
|
467
|
+
64
|
468
|
+
end
|
469
|
+
|
470
|
+
# @return [20] SHA1 digests always have a length of 20 bytes
|
471
|
+
def digest_length
|
472
|
+
20
|
473
|
+
end
|
474
|
+
alias_method :length, :digest_length
|
475
|
+
alias_method :size, :length
|
476
|
+
|
477
|
+
# Hashes the buffer of this SHA1 digest object, returning the computed hash
|
478
|
+
# value.
|
479
|
+
#
|
480
|
+
# @return [String] the hash value of this SHA1 digest object
|
481
|
+
def finish
|
482
|
+
words = @@words.dup
|
483
|
+
generate_split_buffer(@buffer) do |chunk|
|
484
|
+
w = []
|
485
|
+
a, b, c, d, e = *words
|
486
|
+
chunk.each_slice(4) do |a, b, c, d|
|
487
|
+
w << (((a << 8 | b) << 8 | c) << 8 | d)
|
488
|
+
end
|
489
|
+
(16..79).map do |i|
|
490
|
+
w[i] = MASK & rotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1)
|
491
|
+
end
|
492
|
+
0.upto(79) do |i|
|
493
|
+
f, k = case i
|
494
|
+
when 0..19 then [((b & c) | (~b & d)), 0x5A827999]
|
495
|
+
when 20..39 then [(b ^ c ^ d), 0x6ED9EBA1]
|
496
|
+
when 40..59 then [((b & c) | (b & d) | (c & d)), 0x8F1BBCDC]
|
497
|
+
when 60..79 then [(b ^ c ^ d), 0xCA62C1D6]
|
498
|
+
end
|
499
|
+
t = MASK & (MASK & rotate(a, 5) + f + e + k + w[i])
|
500
|
+
a, b, c, d, e = t, a, MASK & rotate(b, 30), c, d
|
501
|
+
end
|
502
|
+
mutated = [a, b, c, d, e]
|
503
|
+
words.map!.with_index { |word, index| MASK & (word + mutated[index]) }
|
504
|
+
end
|
505
|
+
words.reduce('') do |digest, word|
|
506
|
+
digest << [word].pack('N')
|
507
|
+
end.tap { reset }
|
508
|
+
end
|
509
|
+
private :finish
|
510
|
+
|
511
|
+
# Generates a split buffer of integer values used to perform the main loop
|
512
|
+
# of the hashing algorithm.
|
513
|
+
#
|
514
|
+
# @param string [String] the base string to generate a split buffer from
|
515
|
+
# @yieldreturn [Array<Fixnum>] each 64-element chunk of the split buffer
|
516
|
+
# @return [Array<Fixnum>] the split buffer
|
517
|
+
def generate_split_buffer(string)
|
518
|
+
size = string.size * 8
|
519
|
+
buffer = string + ['10000000'].pack('B8')
|
520
|
+
buffer << [0].pack('C') while buffer.size % 64 != 56
|
521
|
+
buffer << [size].pack('Q').reverse
|
522
|
+
buffer = buffer.unpack('C*')
|
523
|
+
block_given? ? buffer.each_slice(64) { |chunk| yield chunk } : buffer
|
524
|
+
end
|
525
|
+
private :generate_split_buffer
|
526
|
+
|
527
|
+
# Binary left-rotates the given `value` by the given number of `spaces`.
|
528
|
+
#
|
529
|
+
# @param value [Fixnum] the value to binary left-rotate
|
530
|
+
# @param spaces [Fixnum] the number of spaces to shift to the left
|
531
|
+
# @return [Fixnum] the left-rotated value
|
532
|
+
def rotate(value, spaces)
|
533
|
+
value << spaces | value >> (32 - spaces)
|
534
|
+
end
|
535
|
+
private :rotate
|
536
|
+
end
|
537
|
+
# SHA256
|
538
|
+
# ===========================================================================
|
539
|
+
# Provides a pure-Ruby implementation of an SHA256 digest object.
|
540
|
+
class SHA256 < Base
|
541
|
+
# The initial constant values for the 32-bit constant words A, B, C, D, E,
|
542
|
+
# F, G, and H, respectively.
|
543
|
+
@@words = [
|
544
|
+
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
|
545
|
+
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19
|
546
|
+
]
|
547
|
+
|
548
|
+
# The constant values used for word mutation each round.
|
549
|
+
#
|
550
|
+
# @note There are 64 rounds per mutation for the SHA256 algorithm.
|
551
|
+
@@rounds = [
|
552
|
+
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1,
|
553
|
+
0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
|
554
|
+
0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786,
|
555
|
+
0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
556
|
+
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147,
|
557
|
+
0x06CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
|
558
|
+
0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B,
|
559
|
+
0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
560
|
+
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A,
|
561
|
+
0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
|
562
|
+
0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
|
563
|
+
]
|
564
|
+
|
565
|
+
# @return [64] SHA256 digests always have a block length of 64 bytes
|
566
|
+
def block_length
|
567
|
+
64
|
568
|
+
end
|
569
|
+
|
570
|
+
# @return [32] SHA256 digests always have a length of 32 bytes
|
571
|
+
def digest_length
|
572
|
+
32
|
573
|
+
end
|
574
|
+
alias_method :length, :digest_length
|
575
|
+
alias_method :size, :length
|
576
|
+
|
577
|
+
# Hashes the buffer of this SHA256 digest object, returning the computed
|
578
|
+
# hash value.
|
579
|
+
#
|
580
|
+
# @return [String] the hash value of this SHA256 digest object
|
581
|
+
def finish
|
582
|
+
words = @@words.dup
|
583
|
+
generate_split_buffer(@buffer) do |chunk|
|
584
|
+
w = []
|
585
|
+
a, b, c, d, e, f, g, h, = *words
|
586
|
+
chunk.each_slice(4) do |a, b, c, d|
|
587
|
+
w << (((a << 8 | b) << 8 | c) << 8 | d)
|
588
|
+
end
|
589
|
+
16.upto(63) do |i|
|
590
|
+
s0 = rotate(w[i - 15], 7) ^ rotate(w[i - 15], 18) ^ (w[i - 15] >> 3)
|
591
|
+
s1 = rotate(w[i - 2], 17) ^ rotate(w[i - 2], 19) ^ (w[i - 2] >> 10)
|
592
|
+
w[i] = MASK & (w[i - 16] + s0 + w[i - 7] + s1)
|
593
|
+
end
|
594
|
+
0.upto(63) do |i|
|
595
|
+
s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)
|
596
|
+
maj = (a & b) ^ (a & c) ^ (b & c)
|
597
|
+
t2 = MASK & (s0 + maj)
|
598
|
+
s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)
|
599
|
+
ch = (e & f) ^ ((~e) & g)
|
600
|
+
t1 = MASK & (h + s1 + ch + @@rounds[i] + w[i])
|
601
|
+
tmp1 = MASK & (t1 + t2)
|
602
|
+
tmp2 = MASK & (d + t1)
|
603
|
+
a, b, c, d, e, f, g, h = tmp1, a, b, c, tmp2, e, f, g
|
604
|
+
end
|
605
|
+
mutated = [a, b, c, d, e, f, g, h]
|
606
|
+
words.map!.with_index { |word, index| MASK & (word + mutated[index]) }
|
607
|
+
end
|
608
|
+
words.reduce('') do |digest, word|
|
609
|
+
digest << [word].pack('N')
|
610
|
+
end.tap { reset }
|
611
|
+
end
|
612
|
+
private :finish
|
613
|
+
|
614
|
+
# Generates a split buffer of integer values used to perform the main loop
|
615
|
+
# of the hashing algorithm.
|
616
|
+
#
|
617
|
+
# @param string [String] the base string to generate a split buffer from
|
618
|
+
# @yieldreturn [Array<Fixnum>] each 64-element chunk of the split buffer
|
619
|
+
# @return [Array<Fixnum>] the split buffer
|
620
|
+
def generate_split_buffer(string)
|
621
|
+
size = string.size * 8
|
622
|
+
buffer = string + ['10000000'].pack('B8')
|
623
|
+
buffer << [0].pack('C') while buffer.size % 64 != 56
|
624
|
+
buffer << [size].pack('Q').reverse
|
625
|
+
buffer = buffer.unpack('C*')
|
626
|
+
block_given? ? buffer.each_slice(64) { |chunk| yield chunk } : buffer
|
627
|
+
end
|
628
|
+
private :generate_split_buffer
|
629
|
+
|
630
|
+
# Binary right-rotates the given `value` by the given number of `spaces`.
|
631
|
+
#
|
632
|
+
# @param value [Fixnum] the value to binary right-rotate
|
633
|
+
# @param spaces [Fixnum] the number of spaces to shift to the right
|
634
|
+
# @return [Fixnum] the right-rotated value
|
635
|
+
def rotate(value, spaces)
|
636
|
+
value >> spaces | value << (32 - spaces)
|
637
|
+
end
|
638
|
+
private :rotate
|
639
|
+
end
|
640
|
+
# SHA2
|
641
|
+
# ===========================================================================
|
642
|
+
# Provides a wrapper class for the SHA2 family of digest objects.
|
643
|
+
class SHA2 < Class
|
644
|
+
# Delegates calls to `#block_length` to the underlying SHA2 digest object.
|
645
|
+
#
|
646
|
+
# @return [Fixnum] the block length of the digest object
|
647
|
+
def block_length
|
648
|
+
@sha2.block_length
|
649
|
+
end
|
650
|
+
|
651
|
+
# Delegates calls to `#digest_length` to the underlying SHA2 digest object.
|
652
|
+
#
|
653
|
+
# @return [Fixnum] the digest length of the digest object
|
654
|
+
def digest_length
|
655
|
+
@sha2.digest_length
|
656
|
+
end
|
657
|
+
alias_method :length, :digest_length
|
658
|
+
alias_method :size, :length
|
659
|
+
|
660
|
+
# Delegates calls to `#finish` to the underlying SHA2 digest object's
|
661
|
+
# `#digest!` instance method.
|
662
|
+
#
|
663
|
+
# @return [String] the resulting hash value of the digest object
|
664
|
+
def finish
|
665
|
+
@sha2.digest!
|
666
|
+
end
|
667
|
+
private :finish
|
668
|
+
|
669
|
+
# Initializes a new {SHA2} digest object of the given `bit_length` with an
|
670
|
+
# empty initial state.
|
671
|
+
#
|
672
|
+
# @param bit_length [Fixnum] the desired SHA2 digest length in bits
|
673
|
+
# @return [self] the new {SHA2} instance
|
674
|
+
# @raise [ArgumentError] if an invalid digest bit length is requested
|
675
|
+
def initialize(bit_length = 256)
|
676
|
+
case bit_length
|
677
|
+
when 256 then @sha2 = Gel::Vendor::RubyDigest::SHA256.new
|
678
|
+
else
|
679
|
+
raise ArgumentError, "unsupported bit length: #{bit_length.inspect}"
|
680
|
+
end
|
681
|
+
@sha2.send(:initialize)
|
682
|
+
@bit_length = bit_length
|
683
|
+
end
|
684
|
+
|
685
|
+
# Customizes duplication of this {SHA2} object, properly setting the digest
|
686
|
+
# object of the duplicate to a copy of the source object's digest object.
|
687
|
+
#
|
688
|
+
# @note This method exists so that duplicate {SHA2} objects do not refer to
|
689
|
+
# the same base digest object as the source digest object.
|
690
|
+
#
|
691
|
+
# @param source [Gel::Vendor::RubyDigest::SHA2] the {SHA2} object being duplicated
|
692
|
+
# @return [Gel::Vendor::RubyDigest::SHA2] the duplicate {SHA2} object
|
693
|
+
def initialize_copy(source)
|
694
|
+
super
|
695
|
+
@sha2 = source.instance_eval { @sha2.clone }
|
696
|
+
end
|
697
|
+
|
698
|
+
# @return [String] a human-readable representation of this {SHA2} instance
|
699
|
+
def inspect
|
700
|
+
"#<#{self.class.name}:#{@bit_length} #{hexdigest}>"
|
701
|
+
end
|
702
|
+
|
703
|
+
# Delegates calls to `#reset` to the underlying SHA2 digest object.
|
704
|
+
#
|
705
|
+
# @return [self] the reset {SHA2} instance
|
706
|
+
def reset
|
707
|
+
tap { @sha2.reset }
|
708
|
+
end
|
709
|
+
|
710
|
+
# Delegates calls to `#update` to the underlying SHA2 digest object.
|
711
|
+
#
|
712
|
+
# @param string [String] the string to update the digest with
|
713
|
+
# @return [self] the updated {SHA2} instance
|
714
|
+
def update(string)
|
715
|
+
tap { @sha2.update(string) }
|
716
|
+
end
|
717
|
+
alias_method :<<, :update
|
718
|
+
end
|
719
|
+
# HMAC
|
720
|
+
# ===========================================================================
|
721
|
+
# Provides a keyed-hash message authentication code object.
|
722
|
+
class HMAC < Class
|
723
|
+
# Delegates calls to `#block_length` to the underlying digest object.
|
724
|
+
#
|
725
|
+
# @return [Fixnum] the block length of the digest object
|
726
|
+
def block_length
|
727
|
+
@md.block_length
|
728
|
+
end
|
729
|
+
|
730
|
+
# Delegates calls to `#digest_length` to the underlying digest object.
|
731
|
+
#
|
732
|
+
# @return [Fixnum] the digest length of the digest object
|
733
|
+
def digest_length
|
734
|
+
@md.digest_length
|
735
|
+
end
|
736
|
+
alias_method :length, :digest_length
|
737
|
+
alias_method :size, :length
|
738
|
+
|
739
|
+
# Delegates calls to `#finish` to the underlying digest object, properly
|
740
|
+
# managing the digest object's buffer for `HMAC`.
|
741
|
+
#
|
742
|
+
# @return [String] the resulting hash value of the digest object
|
743
|
+
def finish
|
744
|
+
original = @md.digest!
|
745
|
+
@md.update(@opad).update(original).digest!
|
746
|
+
end
|
747
|
+
private :finish
|
748
|
+
|
749
|
+
# Initializes a new keyed-hash message authentication code ({HMAC}) object
|
750
|
+
# with the given key and digest object.
|
751
|
+
#
|
752
|
+
# @note {HMAC} objects are significantly more secure than an individual
|
753
|
+
# hashing algorithm on its own.
|
754
|
+
#
|
755
|
+
# @param key [String] the key for this {HMAC} object
|
756
|
+
# @param digest_class [Gel::Vendor::RubyDigest::Base] the digest object for this {HMAC}
|
757
|
+
def initialize(key, digest_class)
|
758
|
+
@md = digest_class.new
|
759
|
+
|
760
|
+
length = @md.block_length
|
761
|
+
key = @md.digest(key) if key.bytesize > length
|
762
|
+
ipad = Array.new(length, 0x36) # Inner HMAC padding.
|
763
|
+
opad = Array.new(length, 0x5C) # Outer HMAC padding.
|
764
|
+
|
765
|
+
key.bytes.each_with_index do |character, index|
|
766
|
+
ipad[index] ^= character
|
767
|
+
opad[index] ^= character
|
768
|
+
end
|
769
|
+
|
770
|
+
@key = key.freeze
|
771
|
+
@ipad = ipad.pack('C*').freeze
|
772
|
+
@opad = opad.pack('C*').freeze
|
773
|
+
@md.update(@ipad)
|
774
|
+
end
|
775
|
+
|
776
|
+
# Customizes duplication of this {HMAC} object, properly setting the digest
|
777
|
+
# object of the duplicate to a copy of the source object's digest object.
|
778
|
+
#
|
779
|
+
# @note This method exists so that duplicate {HMAC} objects do not refer to
|
780
|
+
# the same base digest object as the source digest object.
|
781
|
+
#
|
782
|
+
# @param source [Gel::Vendor::RubyDigest::HMAC] the {HMAC} object being duplicated
|
783
|
+
# @return [Gel::Vendor::RubyDigest::HMAC] the duplicate {HMAC} object
|
784
|
+
def initialize_copy(source)
|
785
|
+
super
|
786
|
+
@md = source.instance_eval { @md.clone }
|
787
|
+
end
|
788
|
+
|
789
|
+
# @return [String] a human-readable representation of this {HMAC} instance
|
790
|
+
def inspect
|
791
|
+
digest = @md.inspect.sub(/^\#<(.*)>$/) { $1 }
|
792
|
+
"#<#{self.class.name}: key=#{@key.inspect} digest=#{digest}>"
|
793
|
+
end
|
794
|
+
|
795
|
+
# Delegates calls to `#reset` to the underlying digest object, properly
|
796
|
+
# updating its contents with the ipad of this {HMAC} object.
|
797
|
+
#
|
798
|
+
# @return [self] the reset {HMAC} instance
|
799
|
+
def reset
|
800
|
+
tap { @md.reset.update(@ipad) }
|
801
|
+
end
|
802
|
+
|
803
|
+
# Delegates calls to `#update` to the underlying digest object.
|
804
|
+
#
|
805
|
+
# @param string [String] the string to update the digest with
|
806
|
+
# @return [self] the updated {HMAC} instance
|
807
|
+
def update(string)
|
808
|
+
tap { @md.update(string) }
|
809
|
+
end
|
810
|
+
alias_method :<<, :update
|
811
|
+
end
|
812
|
+
end
|