http-2 0.12.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ module Header
5
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
6
+ class Compressor
7
+ include PackingExtensions
8
+
9
+ # @param options [Hash] encoding options
10
+ def initialize(options = {})
11
+ @cc = EncodingContext.new(options)
12
+ end
13
+
14
+ # Set dynamic table size in EncodingContext
15
+ # @param size [Integer] new dynamic table size
16
+ def table_size=(size)
17
+ @cc.table_size = size
18
+ end
19
+
20
+ # Encodes provided value via integer representation.
21
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
22
+ #
23
+ # If I < 2^N - 1, encode I on N bits
24
+ # Else
25
+ # encode 2^N - 1 on N bits
26
+ # I = I - (2^N - 1)
27
+ # While I >= 128
28
+ # Encode (I % 128 + 128) on 8 bits
29
+ # I = I / 128
30
+ # encode (I) on 8 bits
31
+ #
32
+ # @param i [Integer] value to encode
33
+ # @param n [Integer] number of available bits
34
+ # @param buffer [String] buffer to pack bytes into
35
+ # @param offset [Integer] offset to insert packed bytes in buffer
36
+ # @return [String] binary string
37
+ def integer(i, n, buffer:, offset: 0)
38
+ limit = (2**n) - 1
39
+ return pack([i], "C", buffer: buffer, offset: offset) if i < limit
40
+
41
+ bytes = []
42
+ bytes.push limit unless n.zero?
43
+
44
+ i -= limit
45
+ while i >= 128
46
+ bytes.push((i % 128) + 128)
47
+ i /= 128
48
+ end
49
+
50
+ bytes.push i
51
+ pack(bytes, "C*", buffer: buffer, offset: offset)
52
+ end
53
+
54
+ # Encodes provided value via string literal representation.
55
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
56
+ #
57
+ # * The string length, defined as the number of bytes needed to store
58
+ # its UTF-8 representation, is represented as an integer with a seven
59
+ # bits prefix. If the string length is strictly less than 127, it is
60
+ # represented as one byte.
61
+ # * If the bit 7 of the first byte is 1, the string value is represented
62
+ # as a list of Huffman encoded octets
63
+ # (padded with bit 1's until next octet boundary).
64
+ # * If the bit 7 of the first byte is 0, the string value is
65
+ # represented as a list of UTF-8 encoded octets.
66
+ #
67
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
68
+ # :never Do not use Huffman encoding
69
+ # :always Always use Huffman encoding
70
+ # :shorter Use Huffman when the result is strictly shorter
71
+ #
72
+ # @param str [String]
73
+ # @return [String] binary string
74
+ def string(str)
75
+ case @cc.options[:huffman]
76
+ when :always
77
+ huffman_string(str)
78
+ when :never
79
+ plain_string(str)
80
+ else
81
+ huffman = huffman_string(str)
82
+
83
+ plain = plain_string(str)
84
+
85
+ huffman.bytesize < plain.bytesize ? huffman : plain
86
+ end
87
+ end
88
+
89
+ # Encodes header command with appropriate header representation.
90
+ #
91
+ # @param h [Hash] header command
92
+ # @param buffer [String]
93
+ # @return [Buffer]
94
+ def header(h, buffer = "".b)
95
+ rep = HEADREP[h[:type]]
96
+
97
+ case h[:type]
98
+ when :indexed
99
+ integer(h[:name] + 1, rep[:prefix], buffer: buffer)
100
+ when :changetablesize
101
+ integer(h[:value], rep[:prefix], buffer: buffer)
102
+ else
103
+ if h[:name].is_a? Integer
104
+ integer(h[:name] + 1, rep[:prefix], buffer: buffer)
105
+ else
106
+ integer(0, rep[:prefix], buffer: buffer)
107
+ buffer << string(h[:name])
108
+ end
109
+
110
+ buffer << string(h[:value])
111
+ end
112
+
113
+ # set header representation pattern on first byte
114
+ fb = buffer.ord | rep[:pattern]
115
+ buffer.setbyte(0, fb)
116
+
117
+ buffer
118
+ end
119
+
120
+ # Encodes provided list of HTTP headers.
121
+ #
122
+ # @param headers [Array] +[[name, value], ...]+
123
+ # @return [Buffer]
124
+ def encode(headers)
125
+ buffer = "".b
126
+ pseudo_headers, regular_headers = headers.partition { |f, _| f.start_with? ":" }
127
+ headers = [*pseudo_headers, *regular_headers]
128
+ commands = @cc.encode(headers)
129
+ commands.each do |cmd|
130
+ buffer << header(cmd)
131
+ end
132
+
133
+ buffer
134
+ end
135
+
136
+ private
137
+
138
+ # @param str [String]
139
+ # @return [String] binary string
140
+ def huffman_string(str)
141
+ huffman = Huffman.new.encode(str)
142
+ integer(huffman.bytesize, 7, buffer: huffman, offset: 0)
143
+ huffman.setbyte(0, huffman.ord | 0x80)
144
+ huffman
145
+ end
146
+
147
+ # @param str [String]
148
+ # @return [String] binary string
149
+ def plain_string(str)
150
+ plain = "".b
151
+ integer(str.bytesize, 7, buffer: plain)
152
+ plain << str.dup.force_encoding(Encoding::BINARY)
153
+ plain
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ module Header
5
+ # Responsible for decoding received headers and maintaining compression
6
+ # context of the opposing peer. Decompressor must be initialized with
7
+ # appropriate starting context based on local role: client or server.
8
+ #
9
+ # @example
10
+ # server_role = Decompressor.new(:request)
11
+ # client_role = Decompressor.new(:response)
12
+ class Decompressor
13
+ include Error
14
+ include BufferUtils
15
+
16
+ # @param options [Hash] decoding options. Only :table_size is effective.
17
+ def initialize(options = {})
18
+ @cc = EncodingContext.new(options)
19
+ end
20
+
21
+ # Set dynamic table size in EncodingContext
22
+ # @param size [Integer] new dynamic table size
23
+ def table_size=(size)
24
+ @cc.table_size = size
25
+ end
26
+
27
+ # Decodes integer value from provided buffer.
28
+ #
29
+ # @param buf [String]
30
+ # @param n [Integer] number of available bits
31
+ # @return [Integer]
32
+ def integer(buf, n)
33
+ limit = (2**n) - 1
34
+ i = n.zero? ? 0 : (shift_byte(buf) & limit)
35
+
36
+ m = 0
37
+ if i == limit
38
+ while (byte = shift_byte(buf))
39
+ i += ((byte & 127) << m)
40
+ m += 7
41
+
42
+ break if byte.nobits?(128)
43
+ end
44
+ end
45
+
46
+ i
47
+ end
48
+
49
+ # Decodes string value from provided buffer.
50
+ #
51
+ # @param buf [String]
52
+ # @return [String] UTF-8 encoded string
53
+ # @raise [CompressionError] when input is malformed
54
+ def string(buf)
55
+ raise CompressionError, "invalid header block fragment" if buf.empty?
56
+
57
+ huffman = buf.getbyte(0).allbits?(0x80)
58
+ len = integer(buf, 7)
59
+ str = read_str(buf, len)
60
+ raise CompressionError, "string too short" unless str.bytesize == len
61
+
62
+ str = Huffman.new.decode(str) if huffman
63
+ str.force_encoding(Encoding::UTF_8)
64
+ end
65
+
66
+ # Decodes header command from provided buffer.
67
+ #
68
+ # @param buf [Buffer]
69
+ # @return [Hash] command
70
+ def header(buf)
71
+ peek = buf.getbyte(0)
72
+
73
+ header = {}
74
+ header[:type], type = HEADREP.find do |_t, desc|
75
+ mask = (peek >> desc[:prefix]) << desc[:prefix]
76
+ mask == desc[:pattern]
77
+ end
78
+
79
+ raise CompressionError unless header[:type]
80
+
81
+ header[:name] = integer(buf, type[:prefix])
82
+
83
+ case header[:type]
84
+ when :indexed
85
+ raise CompressionError if (header[:name]).zero?
86
+
87
+ header[:name] -= 1
88
+ when :changetablesize
89
+ header[:value] = header[:name]
90
+ else
91
+ if (header[:name]).zero?
92
+ header[:name] = string(buf)
93
+ else
94
+ header[:name] -= 1
95
+ end
96
+ header[:value] = string(buf)
97
+ end
98
+
99
+ header
100
+ end
101
+
102
+ FORBIDDEN_HEADERS = %w[connection te].freeze
103
+
104
+ # Decodes and processes header commands within provided buffer.
105
+ #
106
+ # @param buf [Buffer]
107
+ # @param frame [HTTP2::Frame, nil]
108
+ # @return [Array] +[[name, value], ...]
109
+ def decode(buf, frame = nil)
110
+ list = []
111
+ decoding_pseudo_headers = true
112
+ @cc.listen_on_table do
113
+ until buf.empty?
114
+ field, value = @cc.process(header(buf))
115
+ next if field.nil?
116
+
117
+ is_pseudo_header = field.start_with? ":"
118
+ if !decoding_pseudo_headers && is_pseudo_header
119
+ raise ProtocolError, "one or more pseudo headers encountered after regular headers"
120
+ end
121
+
122
+ decoding_pseudo_headers = is_pseudo_header
123
+ raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field)
124
+
125
+ if frame
126
+ case field
127
+ when ":status"
128
+ frame[:status] = Integer(value)
129
+ when ":method"
130
+ frame[:method] = value
131
+ when "content-length"
132
+ frame[:content_length] = Integer(value)
133
+ when "trailer"
134
+ (frame[:trailer] ||= []) << value
135
+ end
136
+ end
137
+ list << [field, value]
138
+ end
139
+ end
140
+ list
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP2
4
+ # To decompress header blocks, a decoder only needs to maintain a
5
+ # dynamic table as a decoding context.
6
+ # No other state information is needed.
7
+ module Header
8
+ class EncodingContext
9
+ include Error
10
+
11
+ UPPER = /[[:upper:]]/.freeze
12
+
13
+ # @private
14
+ # Static table
15
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-A
16
+ STATIC_TABLE = [
17
+ [":authority", ""],
18
+ [":method", "GET"],
19
+ [":method", "POST"],
20
+ [":path", "/"],
21
+ [":path", "/index.html"],
22
+ [":scheme", "http"],
23
+ [":scheme", "https"],
24
+ [":status", "200"],
25
+ [":status", "204"],
26
+ [":status", "206"],
27
+ [":status", "304"],
28
+ [":status", "400"],
29
+ [":status", "404"],
30
+ [":status", "500"],
31
+ ["accept-charset", ""],
32
+ ["accept-encoding", "gzip, deflate"],
33
+ ["accept-language", ""],
34
+ ["accept-ranges", ""],
35
+ ["accept", ""],
36
+ ["access-control-allow-origin", ""],
37
+ ["age", ""],
38
+ ["allow", ""],
39
+ ["authorization", ""],
40
+ ["cache-control", ""],
41
+ ["content-disposition", ""],
42
+ ["content-encoding", ""],
43
+ ["content-language", ""],
44
+ ["content-length", ""],
45
+ ["content-location", ""],
46
+ ["content-range", ""],
47
+ ["content-type", ""],
48
+ ["cookie", ""],
49
+ ["date", ""],
50
+ ["etag", ""],
51
+ ["expect", ""],
52
+ ["expires", ""],
53
+ ["from", ""],
54
+ ["host", ""],
55
+ ["if-match", ""],
56
+ ["if-modified-since", ""],
57
+ ["if-none-match", ""],
58
+ ["if-range", ""],
59
+ ["if-unmodified-since", ""],
60
+ ["last-modified", ""],
61
+ ["link", ""],
62
+ ["location", ""],
63
+ ["max-forwards", ""],
64
+ ["proxy-authenticate", ""],
65
+ ["proxy-authorization", ""],
66
+ ["range", ""],
67
+ ["referer", ""],
68
+ ["refresh", ""],
69
+ ["retry-after", ""],
70
+ ["server", ""],
71
+ ["set-cookie", ""],
72
+ ["strict-transport-security", ""],
73
+ ["transfer-encoding", ""],
74
+ ["user-agent", ""],
75
+ ["vary", ""],
76
+ ["via", ""],
77
+ ["www-authenticate", ""]
78
+ ].each(&:freeze).freeze
79
+
80
+ STATIC_TABLE_BY_FIELD =
81
+ STATIC_TABLE
82
+ .each_with_object({})
83
+ .with_index { |((field, value), hs), idx| (hs[field] ||= []) << [idx, value].freeze }
84
+ .each_value(&:freeze)
85
+ .freeze
86
+
87
+ STATIC_TABLE_SIZE = STATIC_TABLE.size
88
+
89
+ # Current table of header key-value pairs.
90
+ attr_reader :table
91
+
92
+ # Current encoding options
93
+ #
94
+ # :table_size Integer maximum dynamic table size in bytes
95
+ # :huffman Symbol :always, :never, :shorter
96
+ # :index Symbol :all, :static, :never
97
+ attr_reader :options
98
+
99
+ # Initializes compression context with appropriate client/server
100
+ # defaults and maximum size of the dynamic table.
101
+ #
102
+ # @param options [Hash] encoding options
103
+ # :table_size Integer maximum dynamic table size in bytes
104
+ # :huffman Symbol :always, :never, :shorter
105
+ # :index Symbol :all, :static, :never
106
+ def initialize(options = {})
107
+ default_options = {
108
+ huffman: :shorter,
109
+ index: :all,
110
+ table_size: 4096
111
+ }
112
+ @table = []
113
+ @options = default_options.merge(options)
114
+ @limit = @options[:table_size]
115
+ @_table_updated = false
116
+ end
117
+
118
+ # Duplicates current compression context
119
+ # @return [EncodingContext]
120
+ def dup
121
+ other = EncodingContext.new(@options)
122
+ t = @table
123
+ l = @limit
124
+ other.instance_eval do
125
+ @table = t.dup # shallow copy
126
+ @limit = l
127
+ end
128
+ other
129
+ end
130
+
131
+ # Finds an entry in current dynamic table by index.
132
+ # Note that index is zero-based in this module.
133
+ #
134
+ # If the index is greater than the last index in the static table,
135
+ # an entry in the dynamic table is dereferenced.
136
+ #
137
+ # If the index is greater than the last header index, an error is raised.
138
+ #
139
+ # @param index [Integer] zero-based index in the dynamic table.
140
+ # @return [Array] +[key, value]+
141
+ def dereference(index)
142
+ # NOTE: index is zero-based in this module.
143
+ value = STATIC_TABLE[index] || @table[index - STATIC_TABLE_SIZE]
144
+ raise CompressionError, "Index too large" unless value
145
+
146
+ value
147
+ end
148
+
149
+ # Header Block Processing
150
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
151
+ #
152
+ # @param cmd [Hash] { type:, name:, value:, index: }
153
+ # @return [Array, nil] +[name, value]+ header field that is added to the decoded header list,
154
+ # or nil if +cmd[:type]+ is +:changetablesize+
155
+ def process(cmd)
156
+ emit = nil
157
+
158
+ case cmd[:type]
159
+ when :changetablesize
160
+ raise CompressionError, "tried to change table size after adding elements to table" if @_table_updated
161
+
162
+ # we can receive multiple table size change commands inside a header frame. However,
163
+ # we should blow up if we receive another frame where the new table size is bigger.
164
+ table_size_updated = @limit != @options[:table_size]
165
+
166
+ raise CompressionError, "dynamic table size update exceed limit" if !table_size_updated && cmd[:value] > @limit
167
+
168
+ self.table_size = cmd[:value]
169
+
170
+ when :indexed
171
+ # Indexed Representation
172
+ # An _indexed representation_ entails the following actions:
173
+ # o The header field corresponding to the referenced entry in either
174
+ # the static table or dynamic table is added to the decoded header
175
+ # list.
176
+ idx = cmd[:name]
177
+
178
+ k, v = dereference(idx)
179
+ emit = [k, v]
180
+
181
+ when :incremental, :noindex, :neverindexed
182
+ # A _literal representation_ that is _not added_ to the dynamic table
183
+ # entails the following action:
184
+ # o The header field is added to the decoded header list.
185
+
186
+ # A _literal representation_ that is _added_ to the dynamic table
187
+ # entails the following actions:
188
+ # o The header field is added to the decoded header list.
189
+ # o The header field is inserted at the beginning of the dynamic table.
190
+
191
+ case cmd[:name]
192
+ when Integer
193
+ k, v = dereference(cmd[:name])
194
+
195
+ cmd = cmd.dup
196
+ cmd[:index] ||= cmd[:name]
197
+ cmd[:value] ||= v
198
+ cmd[:name] = k
199
+ when UPPER
200
+ raise ProtocolError, "Invalid uppercase key: #{cmd[:name]}"
201
+ end
202
+
203
+ emit = [cmd[:name], cmd[:value]]
204
+
205
+ add_to_table(emit) if cmd[:type] == :incremental
206
+
207
+ else
208
+ raise CompressionError, "Invalid type: #{cmd[:type]}"
209
+ end
210
+
211
+ emit
212
+ end
213
+
214
+ # Plan header compression according to +@options [:index]+
215
+ # :never Do not use dynamic table or static table reference at all.
216
+ # :static Use static table only.
217
+ # :all Use all of them.
218
+ #
219
+ # @param headers [Array] +[[name, value], ...]+
220
+ # @return [Array] array of commands
221
+ def encode(headers)
222
+ commands = []
223
+ # Literals commands are marked with :noindex when index is not used
224
+ noindex = %i[static never].include?(@options[:index])
225
+ headers.each do |field, value|
226
+ # Literal header names MUST be translated to lowercase before
227
+ # encoding and transmission.
228
+ field = field.downcase if UPPER.match?(field)
229
+ value = "/" if field == ":path" && value.empty?
230
+ cmd = addcmd(field, value)
231
+ cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
232
+ commands << cmd
233
+ process(cmd)
234
+ end
235
+ commands
236
+ end
237
+
238
+ # Emits command for a header.
239
+ # Prefer static table over dynamic table.
240
+ # Prefer exact match over name-only match.
241
+ #
242
+ # +@options [:index]+ controls whether to use the dynamic table,
243
+ # static table, or both.
244
+ # :never Do not use dynamic table or static table reference at all.
245
+ # :static Use static table only.
246
+ # :all Use all of them.
247
+ #
248
+ # @param header [Array] +[name, value]+
249
+ # @return [Hash] command
250
+ def addcmd(*header)
251
+ exact = nil
252
+ name_only = nil
253
+
254
+ if %i[all static].include?(@options[:index])
255
+ field, value = header
256
+ if (svalues = STATIC_TABLE_BY_FIELD[field])
257
+ svalues.each do |i, svalue|
258
+ name_only ||= i
259
+ if svalue == value
260
+ exact = i
261
+ break
262
+ end
263
+ end
264
+ end
265
+ end
266
+ if [:all].include?(@options[:index]) && !exact
267
+ @table.each_index do |i|
268
+ if @table[i] == header
269
+ exact ||= i + STATIC_TABLE_SIZE
270
+ break
271
+ elsif @table[i].first == header.first
272
+ name_only ||= i + STATIC_TABLE_SIZE
273
+ end
274
+ end
275
+ end
276
+
277
+ if exact
278
+ { name: exact, type: :indexed }
279
+ elsif name_only
280
+ { name: name_only, value: header.last, type: :incremental }
281
+ else
282
+ { name: header.first, value: header.last, type: :incremental }
283
+ end
284
+ end
285
+
286
+ # Alter dynamic table size.
287
+ # When the size is reduced, some headers might be evicted.
288
+ def table_size=(size)
289
+ @limit = size
290
+ size_check(nil)
291
+ end
292
+
293
+ # Returns current table size in octets
294
+ # @return [Integer]
295
+ def current_table_size
296
+ @table.inject(0) { |r, (k, v)| r + k.bytesize + v.bytesize + 32 }
297
+ end
298
+
299
+ def listen_on_table
300
+ yield
301
+ ensure
302
+ @_table_updated = false
303
+ end
304
+
305
+ private
306
+
307
+ # Add a name-value pair to the dynamic table.
308
+ # Older entries might have been evicted so that
309
+ # the new entry fits in the dynamic table.
310
+ #
311
+ # @param cmd [Array] +[name, value]+
312
+ def add_to_table(cmd)
313
+ return unless size_check(cmd)
314
+
315
+ @table.unshift(cmd)
316
+ @_table_updated = true
317
+ end
318
+
319
+ # To keep the dynamic table size lower than or equal to @limit,
320
+ # remove one or more entries at the end of the dynamic table.
321
+ #
322
+ # @param cmd [Hash]
323
+ # @return [Boolean] whether +cmd+ fits in the dynamic table.
324
+ def size_check(cmd)
325
+ cursize = current_table_size
326
+ cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
327
+
328
+ while cursize + cmdsize > @limit
329
+ break if @table.empty?
330
+
331
+ e = @table.pop
332
+ cursize -= e[0].bytesize + e[1].bytesize + 32
333
+ end
334
+
335
+ cmdsize <= @limit
336
+ end
337
+ end
338
+ end
339
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'error'
3
+ require_relative "../error"
4
+ require_relative "../extensions"
4
5
 
5
6
  module HTTP2
6
7
  # Implementation of huffman encoding for HPACK
@@ -10,6 +11,7 @@ module HTTP2
10
11
  # Huffman encoder/decoder
11
12
  class Huffman
12
13
  include Error
14
+ include PackingExtensions
13
15
 
14
16
  BITS_AT_ONCE = 4
15
17
  EOS = 256
@@ -22,8 +24,8 @@ module HTTP2
22
24
  # @return [String] binary string
23
25
  def encode(str)
24
26
  bitstring = str.each_byte.map { |chr| ENCODE_TABLE[chr] }.join
25
- bitstring << ('1' * ((8 - bitstring.size) % 8))
26
- [bitstring].pack('B*')
27
+ bitstring << ("1" * ((8 - bitstring.size) % 8))
28
+ [bitstring].pack("B*")
27
29
  end
28
30
 
29
31
  # Decodes provided Huffman coded string.
@@ -32,7 +34,7 @@ module HTTP2
32
34
  # @return [String] binary string
33
35
  # @raise [CompressionError] when Huffman coded string is malformed
34
36
  def decode(buf)
35
- emit = String.new
37
+ emit = "".b
36
38
  state = 0 # start state
37
39
 
38
40
  mask = (1 << BITS_AT_ONCE) - 1
@@ -45,14 +47,14 @@ module HTTP2
45
47
  # [emit] character to be emitted on this transition, empty string, or EOS.
46
48
  # [next] next state number.
47
49
  trans = MACHINE[state][branch]
48
- raise CompressionError, 'Huffman decode error (EOS found)' if trans.first == EOS
50
+ raise CompressionError, "Huffman decode error (EOS found)" if trans.first == EOS
49
51
 
50
52
  emit << trans.first.chr if trans.first
51
53
  state = trans.last
52
54
  end
53
55
  end
54
56
  # Check whether partial input is correctly filled
55
- raise CompressionError, 'Huffman decode error (EOS invalid)' unless state <= MAX_FINAL_STATE
57
+ raise CompressionError, "Huffman decode error (EOS invalid)" unless state <= MAX_FINAL_STATE
56
58
 
57
59
  emit.force_encoding(Encoding::BINARY)
58
60
  end
@@ -319,7 +321,7 @@ module HTTP2
319
321
  [0x3fffffff, 30]
320
322
  ].each(&:freeze).freeze
321
323
 
322
- ENCODE_TABLE = CODES.map { |c, l| [c].pack('N').unpack1('B*')[-l..-1] }.each(&:freeze).freeze
324
+ ENCODE_TABLE = CODES.map { |c, l| [c].pack("N").unpack1("B*")[-l..-1] }.each(&:freeze).freeze
323
325
  end
324
326
  end
325
327
  end