http-2 0.12.0 → 1.0.0

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
+ using StringExtensions
6
+ # Responsible for decoding received headers and maintaining compression
7
+ # context of the opposing peer. Decompressor must be initialized with
8
+ # appropriate starting context based on local role: client or server.
9
+ #
10
+ # @example
11
+ # server_role = Decompressor.new(:request)
12
+ # client_role = Decompressor.new(:response)
13
+ class Decompressor
14
+ include Error
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 : (buf.shift_byte & limit)
35
+
36
+ m = 0
37
+ if i == limit
38
+ while (byte = buf.shift_byte)
39
+ i += ((byte & 127) << m)
40
+ m += 7
41
+
42
+ break if (byte & 128).zero?
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) & 0x80) == 0x80
58
+ len = integer(buf, 7)
59
+ str = buf.read(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,337 @@
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 { |pair| pair.each(&:freeze).freeze }.freeze
79
+
80
+ STATIC_TABLE_BY_FIELD = STATIC_TABLE
81
+ .each_with_object({})
82
+ .with_index { |((field, value), hs), idx| (hs[field] ||= []) << [idx, value] }
83
+ .each { |pair| pair.each(&:freeze).freeze }.freeze
84
+
85
+ STATIC_TABLE_SIZE = STATIC_TABLE.size
86
+
87
+ # Current table of header key-value pairs.
88
+ attr_reader :table
89
+
90
+ # Current encoding options
91
+ #
92
+ # :table_size Integer maximum dynamic table size in bytes
93
+ # :huffman Symbol :always, :never, :shorter
94
+ # :index Symbol :all, :static, :never
95
+ attr_reader :options
96
+
97
+ # Initializes compression context with appropriate client/server
98
+ # defaults and maximum size of the dynamic table.
99
+ #
100
+ # @param options [Hash] encoding options
101
+ # :table_size Integer maximum dynamic table size in bytes
102
+ # :huffman Symbol :always, :never, :shorter
103
+ # :index Symbol :all, :static, :never
104
+ def initialize(options = {})
105
+ default_options = {
106
+ huffman: :shorter,
107
+ index: :all,
108
+ table_size: 4096
109
+ }
110
+ @table = []
111
+ @options = default_options.merge(options)
112
+ @limit = @options[:table_size]
113
+ @_table_updated = false
114
+ end
115
+
116
+ # Duplicates current compression context
117
+ # @return [EncodingContext]
118
+ def dup
119
+ other = EncodingContext.new(@options)
120
+ t = @table
121
+ l = @limit
122
+ other.instance_eval do
123
+ @table = t.dup # shallow copy
124
+ @limit = l
125
+ end
126
+ other
127
+ end
128
+
129
+ # Finds an entry in current dynamic table by index.
130
+ # Note that index is zero-based in this module.
131
+ #
132
+ # If the index is greater than the last index in the static table,
133
+ # an entry in the dynamic table is dereferenced.
134
+ #
135
+ # If the index is greater than the last header index, an error is raised.
136
+ #
137
+ # @param index [Integer] zero-based index in the dynamic table.
138
+ # @return [Array] +[key, value]+
139
+ def dereference(index)
140
+ # NOTE: index is zero-based in this module.
141
+ value = STATIC_TABLE[index] || @table[index - STATIC_TABLE_SIZE]
142
+ raise CompressionError, "Index too large" unless value
143
+
144
+ value
145
+ end
146
+
147
+ # Header Block Processing
148
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
149
+ #
150
+ # @param cmd [Hash] { type:, name:, value:, index: }
151
+ # @return [Array, nil] +[name, value]+ header field that is added to the decoded header list,
152
+ # or nil if +cmd[:type]+ is +:changetablesize+
153
+ def process(cmd)
154
+ emit = nil
155
+
156
+ case cmd[:type]
157
+ when :changetablesize
158
+ raise CompressionError, "tried to change table size after adding elements to table" if @_table_updated
159
+
160
+ # we can receive multiple table size change commands inside a header frame. However,
161
+ # we should blow up if we receive another frame where the new table size is bigger.
162
+ table_size_updated = @limit != @options[:table_size]
163
+
164
+ raise CompressionError, "dynamic table size update exceed limit" if !table_size_updated && cmd[:value] > @limit
165
+
166
+ self.table_size = cmd[:value]
167
+
168
+ when :indexed
169
+ # Indexed Representation
170
+ # An _indexed representation_ entails the following actions:
171
+ # o The header field corresponding to the referenced entry in either
172
+ # the static table or dynamic table is added to the decoded header
173
+ # list.
174
+ idx = cmd[:name]
175
+
176
+ k, v = dereference(idx)
177
+ emit = [k, v]
178
+
179
+ when :incremental, :noindex, :neverindexed
180
+ # A _literal representation_ that is _not added_ to the dynamic table
181
+ # entails the following action:
182
+ # o The header field is added to the decoded header list.
183
+
184
+ # A _literal representation_ that is _added_ to the dynamic table
185
+ # entails the following actions:
186
+ # o The header field is added to the decoded header list.
187
+ # o The header field is inserted at the beginning of the dynamic table.
188
+
189
+ case cmd[:name]
190
+ when Integer
191
+ k, v = dereference(cmd[:name])
192
+
193
+ cmd = cmd.dup
194
+ cmd[:index] ||= cmd[:name]
195
+ cmd[:value] ||= v
196
+ cmd[:name] = k
197
+ when UPPER
198
+ raise ProtocolError, "Invalid uppercase key: #{cmd[:name]}"
199
+ end
200
+
201
+ emit = [cmd[:name], cmd[:value]]
202
+
203
+ add_to_table(emit) if cmd[:type] == :incremental
204
+
205
+ else
206
+ raise CompressionError, "Invalid type: #{cmd[:type]}"
207
+ end
208
+
209
+ emit
210
+ end
211
+
212
+ # Plan header compression according to +@options [:index]+
213
+ # :never Do not use dynamic table or static table reference at all.
214
+ # :static Use static table only.
215
+ # :all Use all of them.
216
+ #
217
+ # @param headers [Array] +[[name, value], ...]+
218
+ # @return [Array] array of commands
219
+ def encode(headers)
220
+ commands = []
221
+ # Literals commands are marked with :noindex when index is not used
222
+ noindex = %i[static never].include?(@options[:index])
223
+ headers.each do |field, value|
224
+ # Literal header names MUST be translated to lowercase before
225
+ # encoding and transmission.
226
+ field = field.downcase if UPPER.match?(field)
227
+ value = "/" if field == ":path" && value.empty?
228
+ cmd = addcmd(field, value)
229
+ cmd[:type] = :noindex if noindex && cmd[:type] == :incremental
230
+ commands << cmd
231
+ process(cmd)
232
+ end
233
+ commands
234
+ end
235
+
236
+ # Emits command for a header.
237
+ # Prefer static table over dynamic table.
238
+ # Prefer exact match over name-only match.
239
+ #
240
+ # +@options [:index]+ controls whether to use the dynamic table,
241
+ # static table, or both.
242
+ # :never Do not use dynamic table or static table reference at all.
243
+ # :static Use static table only.
244
+ # :all Use all of them.
245
+ #
246
+ # @param header [Array] +[name, value]+
247
+ # @return [Hash] command
248
+ def addcmd(*header)
249
+ exact = nil
250
+ name_only = nil
251
+
252
+ if %i[all static].include?(@options[:index])
253
+ field, value = header
254
+ if (svalues = STATIC_TABLE_BY_FIELD[field])
255
+ svalues.each do |i, svalue|
256
+ name_only ||= i
257
+ if svalue == value
258
+ exact = i
259
+ break
260
+ end
261
+ end
262
+ end
263
+ end
264
+ if [:all].include?(@options[:index]) && !exact
265
+ @table.each_index do |i|
266
+ if @table[i] == header
267
+ exact ||= i + STATIC_TABLE_SIZE
268
+ break
269
+ elsif @table[i].first == header.first
270
+ name_only ||= i + STATIC_TABLE_SIZE
271
+ end
272
+ end
273
+ end
274
+
275
+ if exact
276
+ { name: exact, type: :indexed }
277
+ elsif name_only
278
+ { name: name_only, value: header.last, type: :incremental }
279
+ else
280
+ { name: header.first, value: header.last, type: :incremental }
281
+ end
282
+ end
283
+
284
+ # Alter dynamic table size.
285
+ # When the size is reduced, some headers might be evicted.
286
+ def table_size=(size)
287
+ @limit = size
288
+ size_check(nil)
289
+ end
290
+
291
+ # Returns current table size in octets
292
+ # @return [Integer]
293
+ def current_table_size
294
+ @table.inject(0) { |r, (k, v)| r + k.bytesize + v.bytesize + 32 }
295
+ end
296
+
297
+ def listen_on_table
298
+ yield
299
+ ensure
300
+ @_table_updated = false
301
+ end
302
+
303
+ private
304
+
305
+ # Add a name-value pair to the dynamic table.
306
+ # Older entries might have been evicted so that
307
+ # the new entry fits in the dynamic table.
308
+ #
309
+ # @param cmd [Array] +[name, value]+
310
+ def add_to_table(cmd)
311
+ return unless size_check(cmd)
312
+
313
+ @table.unshift(cmd)
314
+ @_table_updated = true
315
+ end
316
+
317
+ # To keep the dynamic table size lower than or equal to @limit,
318
+ # remove one or more entries at the end of the dynamic table.
319
+ #
320
+ # @param cmd [Hash]
321
+ # @return [Boolean] whether +cmd+ fits in the dynamic table.
322
+ def size_check(cmd)
323
+ cursize = current_table_size
324
+ cmdsize = cmd.nil? ? 0 : cmd[0].bytesize + cmd[1].bytesize + 32
325
+
326
+ while cursize + cmdsize > @limit
327
+ break if @table.empty?
328
+
329
+ e = @table.pop
330
+ cursize -= e[0].bytesize + e[1].bytesize + 32
331
+ end
332
+
333
+ cmdsize <= @limit
334
+ end
335
+ end
336
+ end
337
+ 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
@@ -9,7 +10,10 @@ module HTTP2
9
10
  module Header
10
11
  # Huffman encoder/decoder
11
12
  class Huffman
13
+ using StringExtensions
14
+
12
15
  include Error
16
+ include PackingExtensions
13
17
 
14
18
  BITS_AT_ONCE = 4
15
19
  EOS = 256
@@ -22,8 +26,8 @@ module HTTP2
22
26
  # @return [String] binary string
23
27
  def encode(str)
24
28
  bitstring = str.each_byte.map { |chr| ENCODE_TABLE[chr] }.join
25
- bitstring << ('1' * ((8 - bitstring.size) % 8))
26
- [bitstring].pack('B*')
29
+ bitstring << ("1" * ((8 - bitstring.size) % 8))
30
+ [bitstring].pack("B*")
27
31
  end
28
32
 
29
33
  # Decodes provided Huffman coded string.
@@ -32,7 +36,7 @@ module HTTP2
32
36
  # @return [String] binary string
33
37
  # @raise [CompressionError] when Huffman coded string is malformed
34
38
  def decode(buf)
35
- emit = String.new
39
+ emit = "".b
36
40
  state = 0 # start state
37
41
 
38
42
  mask = (1 << BITS_AT_ONCE) - 1
@@ -45,14 +49,14 @@ module HTTP2
45
49
  # [emit] character to be emitted on this transition, empty string, or EOS.
46
50
  # [next] next state number.
47
51
  trans = MACHINE[state][branch]
48
- raise CompressionError, 'Huffman decode error (EOS found)' if trans.first == EOS
52
+ raise CompressionError, "Huffman decode error (EOS found)" if trans.first == EOS
49
53
 
50
54
  emit << trans.first.chr if trans.first
51
55
  state = trans.last
52
56
  end
53
57
  end
54
58
  # Check whether partial input is correctly filled
55
- raise CompressionError, 'Huffman decode error (EOS invalid)' unless state <= MAX_FINAL_STATE
59
+ raise CompressionError, "Huffman decode error (EOS invalid)" unless state <= MAX_FINAL_STATE
56
60
 
57
61
  emit.force_encoding(Encoding::BINARY)
58
62
  end
@@ -319,7 +323,7 @@ module HTTP2
319
323
  [0x3fffffff, 30]
320
324
  ].each(&:freeze).freeze
321
325
 
322
- ENCODE_TABLE = CODES.map { |c, l| [c].pack('N').unpack1('B*')[-l..-1] }.each(&:freeze).freeze
326
+ ENCODE_TABLE = CODES.map { |c, l| [c].pack("N").unpack1("B*")[-l..-1] }.each(&:freeze).freeze
323
327
  end
324
328
  end
325
329
  end