http-hpack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29b1f18fa2d9df85696ed6947a352cd458b9621b24a07f0bdb60b545f89b170f
4
+ data.tar.gz: 89bc9927bbe44b282132d284e07bd7d130f324d4706340e3ec4f6c559cc6c55d
5
+ SHA512:
6
+ metadata.gz: ecf69ccff7790a4ddf0c4ae6064898e969ce841df18629ab3e1aa6d15266a664196c9b2a7464e3d9b4492b54ec525b2414e89abd5f1127dfb606cf624c162179
7
+ data.tar.gz: a6a13dc0c7534812e363d433a3fa3e54d9121c3276a7eb90d8044ee58b0c471c814e40903ac2040235dd8475ab32252f8853cbd6cbd977b4d950ea0aee24fbe1
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
@@ -0,0 +1,3 @@
1
+ [submodule "spec/http/hpack/hpack-test-case"]
2
+ path = spec/http/hpack/hpack-test-case
3
+ url = https://github.com/http2jp/hpack-test-case.git
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --warnings
3
+ --require spec_helper
@@ -0,0 +1,23 @@
1
+ language: ruby
2
+ sudo: required
3
+ dist: xenial
4
+ cache: bundler
5
+
6
+ before_script:
7
+ - gem update --system
8
+ - gem install bundler
9
+
10
+ matrix:
11
+ include:
12
+ - rvm: 2.3
13
+ - rvm: 2.4
14
+ - rvm: 2.5
15
+ - rvm: 2.6
16
+ - rvm: jruby-head
17
+ env: JRUBY_OPTS="--debug -X+O"
18
+ - rvm: ruby-head
19
+ - rvm: rbx-3
20
+ allow_failures:
21
+ - rvm: ruby-head
22
+ - rvm: jruby-head
23
+ - rvm: rbx-3
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in http-hpack.gemspec
6
+ gemspec
7
+
8
+ group :test do
9
+ gem 'covered', require: 'covered/rspec' if RUBY_VERSION >= "2.6.0"
10
+ end
@@ -0,0 +1,75 @@
1
+ # HTTP::HPACK
2
+
3
+ Provides a compressor and decompressor for HTTP 2.0 headers, HPACK, as defined by [RFC7541](https://tools.ietf.org/html/rfc7541).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'http-hpack'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install http-hpack
20
+
21
+ ## Usage
22
+
23
+ ### Compressing Headers
24
+
25
+ ```ruby
26
+ buffer = String.new.b
27
+ compressor = HTTP::HPACK::Compressor.new(buffer)
28
+
29
+ compressor.encode([['content-length', '5']])
30
+ => "\\\x015"
31
+ ```
32
+
33
+ ### Decompressing Headers
34
+
35
+ Reusing `buffer` from above:
36
+
37
+ ```ruby
38
+ decompressor = HTTP::HPACK::Decompressor.new(buffer)
39
+
40
+ decompressor.decode
41
+ => [["content-length", "5"]]
42
+ ```
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
51
+
52
+ ## License
53
+
54
+ Released under the MIT license.
55
+
56
+ Copyright, 2018, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
57
+ Copyrigh, 2013, by Ilya Grigorik.
58
+
59
+ Permission is hereby granted, free of charge, to any person obtaining a copy
60
+ of this software and associated documentation files (the "Software"), to deal
61
+ in the Software without restriction, including without limitation the rights
62
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
63
+ copies of the Software, and to permit persons to whom the Software is
64
+ furnished to do so, subject to the following conditions:
65
+
66
+ The above copyright notice and this permission notice shall be included in
67
+ all copies or substantial portions of the Software.
68
+
69
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
70
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
71
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
72
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
73
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
74
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
75
+ THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ load *Dir["tasks/*.rake"]
@@ -0,0 +1,22 @@
1
+
2
+ require_relative "lib/http/hpack/version"
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "http-hpack"
6
+ spec.version = HTTP::HPACK::VERSION
7
+ spec.authors = ["Samuel Williams"]
8
+ spec.email = ["samuel.williams@oriontransfer.co.nz"]
9
+
10
+ spec.summary = "A compresssor and decompressor for HTTP 2.0 HPACK."
11
+ spec.homepage = "https://github.com/socketry/http-hpack"
12
+
13
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
14
+ f.match(%r{^(test|spec|features)/})
15
+ end
16
+
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.16"
20
+ spec.add_development_dependency "rake", "~> 10.0"
21
+ spec.add_development_dependency "rspec", "~> 3.0"
22
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require "http/hpack/version"
@@ -0,0 +1,193 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ # Copyrigh, 2013, by Ilya Grigorik.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require_relative 'context'
23
+ require_relative 'huffman'
24
+
25
+ module HTTP
26
+ module HPACK
27
+ # Predefined options set for Compressor
28
+ # http://mew.org/~kazu/material/2014-hpack.pdf
29
+ NAIVE = {index: :never, huffman: :never}
30
+ LINEAR = {index: :all, huffman: :never}
31
+ STATIC = {index: :static, huffman: :never}
32
+ SHORTER = {index: :all, huffman: :never}
33
+ NAIVE_HUFFMAN = {index: :never, huffman: :always}
34
+ LINEAR_HUFFMAN = {index: :all, huffman: :always}
35
+ STATIC_HUFFMAN = {index: :static, huffman: :always}
36
+ SHORTER_HUFFMAN = {index: :all, huffman: :shorter}
37
+
38
+ MODES = {
39
+ naive: NAIVE,
40
+ linear: LINEAR,
41
+ static: STATIC,
42
+ shorter: SHORTER,
43
+ naive_huffman: NAIVE_HUFFMAN,
44
+ linear_huffman: NAIVE_HUFFMAN,
45
+ static_huffman: NAIVE_HUFFMAN,
46
+ shorter_huffman: NAIVE_HUFFMAN,
47
+ }
48
+
49
+ # Responsible for encoding header key-value pairs using HPACK algorithm.
50
+ class Compressor
51
+ def initialize(buffer, context = Context.new)
52
+ @buffer = buffer
53
+ @context = context
54
+ end
55
+
56
+ attr :buffer
57
+ attr :context
58
+ attr :offset
59
+
60
+ def write_byte(byte)
61
+ @buffer << byte.chr
62
+ end
63
+
64
+ def write_bytes(bytes)
65
+ @buffer << bytes
66
+ end
67
+
68
+ # Encodes provided value via integer representation.
69
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.1
70
+ #
71
+ # If I < 2^N - 1, encode I on N bits
72
+ # Else
73
+ # encode 2^N - 1 on N bits
74
+ # I = I - (2^N - 1)
75
+ # While I >= 128
76
+ # Encode (I % 128 + 128) on 8 bits
77
+ # I = I / 128
78
+ # encode (I) on 8 bits
79
+ #
80
+ # @param value [Integer] value to encode
81
+ # @param bits [Integer] number of available bits
82
+ # @return [String] binary string
83
+ def write_integer(value, bits)
84
+ limit = 2**bits - 1
85
+
86
+ return write_bytes([value].pack('C')) if value < limit
87
+
88
+ bytes = []
89
+ bytes.push(limit) unless bits.zero?
90
+
91
+ value -= limit
92
+ while value >= 128
93
+ bytes.push((value % 128) + 128)
94
+ value /= 128
95
+ end
96
+
97
+ bytes.push(value)
98
+
99
+ write_bytes(bytes.pack('C*'))
100
+ end
101
+
102
+ def huffman
103
+ @context.options[:huffman]
104
+ end
105
+
106
+ # Encodes provided value via string literal representation.
107
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
108
+ #
109
+ # * The string length, defined as the number of bytes needed to store
110
+ # its UTF-8 representation, is represented as an integer with a seven
111
+ # bits prefix. If the string length is strictly less than 127, it is
112
+ # represented as one byte.
113
+ # * If the bit 7 of the first byte is 1, the string value is represented
114
+ # as a list of Huffman encoded octets
115
+ # (padded with bit 1's until next octet boundary).
116
+ # * If the bit 7 of the first byte is 0, the string value is
117
+ # represented as a list of UTF-8 encoded octets.
118
+ #
119
+ # +@options [:huffman]+ controls whether to use Huffman encoding:
120
+ # :never Do not use Huffman encoding
121
+ # :always Always use Huffman encoding
122
+ # :shorter Use Huffman when the result is strictly shorter
123
+ #
124
+ # @param string [String]
125
+ # @return [String] binary string
126
+ def write_string(string, huffman = self.huffman)
127
+ if huffman != :never
128
+ encoded = Huffman.new.encode(string)
129
+
130
+ if huffman == :shorter and encoded.bytesize >= string.bytesize
131
+ encoded = nil
132
+ end
133
+ end
134
+
135
+ if encoded
136
+ first = @buffer.bytesize
137
+
138
+ write_integer(encoded.bytesize, 7)
139
+ write_bytes(encoded.b)
140
+
141
+ @buffer.setbyte(first, @buffer.getbyte(first).ord | 0x80)
142
+ else
143
+ write_integer(string.bytesize, 7)
144
+ write_bytes(string.b)
145
+ end
146
+ end
147
+
148
+ # Encodes header command with appropriate header representation.
149
+ #
150
+ # @param h [Hash] header command
151
+ # @param buffer [String]
152
+ # @return [Buffer]
153
+ def write_header(command)
154
+ representation = HEADER_REPRESENTATION[command[:type]]
155
+
156
+ first = @buffer.bytesize
157
+
158
+ case command[:type]
159
+ when :indexed
160
+ write_integer(command[:name] + 1, representation[:prefix])
161
+ when :changetablesize
162
+ write_integer(command[:value], representation[:prefix])
163
+ else
164
+ if command[:name].is_a? Integer
165
+ write_integer(command[:name] + 1, representation[:prefix])
166
+ else
167
+ write_integer(0, representation[:prefix])
168
+ write_string(command[:name])
169
+ end
170
+
171
+ write_string(command[:value])
172
+ end
173
+
174
+ # set header representation pattern on first byte
175
+ @buffer.setbyte(first, @buffer.getbyte(first) | representation[:pattern])
176
+ end
177
+
178
+ # Encodes provided list of HTTP headers.
179
+ #
180
+ # @param headers [Array] +[[name, value], ...]+
181
+ # @return [Buffer]
182
+ def encode(headers)
183
+ commands = @context.encode(headers)
184
+
185
+ commands.each do |command|
186
+ write_header(command)
187
+ end
188
+
189
+ return @buffer
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,337 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ # Copyrigh, 2013, by Ilya Grigorik.
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+
22
+ require_relative 'huffman'
23
+
24
+ module HTTP
25
+ # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
26
+ # to efficiently represent HTTP headers in the context of HTTP 2.0.
27
+ #
28
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
29
+ module HPACK
30
+ # Header representation as defined by the spec.
31
+ HEADER_REPRESENTATION = {
32
+ indexed: {prefix: 7, pattern: 0x80},
33
+ incremental: {prefix: 6, pattern: 0x40},
34
+ noindex: {prefix: 4, pattern: 0x00},
35
+ neverindexed: {prefix: 4, pattern: 0x10},
36
+ changetablesize: {prefix: 5, pattern: 0x20},
37
+ }
38
+
39
+ # To decompress header blocks, a decoder only needs to maintain a
40
+ # dynamic table as a decoding context.
41
+ # No other state information is needed.
42
+ class Context
43
+ # @private
44
+ # Static table
45
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-A
46
+ STATIC_TABLE = [
47
+ [':authority', ''],
48
+ [':method', 'GET'],
49
+ [':method', 'POST'],
50
+ [':path', '/'],
51
+ [':path', '/index.html'],
52
+ [':scheme', 'http'],
53
+ [':scheme', 'https'],
54
+ [':status', '200'],
55
+ [':status', '204'],
56
+ [':status', '206'],
57
+ [':status', '304'],
58
+ [':status', '400'],
59
+ [':status', '404'],
60
+ [':status', '500'],
61
+ ['accept-charset', ''],
62
+ ['accept-encoding', 'gzip, deflate'],
63
+ ['accept-language', ''],
64
+ ['accept-ranges', ''],
65
+ ['accept', ''],
66
+ ['access-control-allow-origin', ''],
67
+ ['age', ''],
68
+ ['allow', ''],
69
+ ['authorization', ''],
70
+ ['cache-control', ''],
71
+ ['content-disposition', ''],
72
+ ['content-encoding', ''],
73
+ ['content-language', ''],
74
+ ['content-length', ''],
75
+ ['content-location', ''],
76
+ ['content-range', ''],
77
+ ['content-type', ''],
78
+ ['cookie', ''],
79
+ ['date', ''],
80
+ ['etag', ''],
81
+ ['expect', ''],
82
+ ['expires', ''],
83
+ ['from', ''],
84
+ ['host', ''],
85
+ ['if-match', ''],
86
+ ['if-modified-since', ''],
87
+ ['if-none-match', ''],
88
+ ['if-range', ''],
89
+ ['if-unmodified-since', ''],
90
+ ['last-modified', ''],
91
+ ['link', ''],
92
+ ['location', ''],
93
+ ['max-forwards', ''],
94
+ ['proxy-authenticate', ''],
95
+ ['proxy-authorization', ''],
96
+ ['range', ''],
97
+ ['referer', ''],
98
+ ['refresh', ''],
99
+ ['retry-after', ''],
100
+ ['server', ''],
101
+ ['set-cookie', ''],
102
+ ['strict-transport-security', ''],
103
+ ['transfer-encoding', ''],
104
+ ['user-agent', ''],
105
+ ['vary', ''],
106
+ ['via', ''],
107
+ ['www-authenticate', ''],
108
+ ].each {|pair| pair.each(&:freeze).freeze}.freeze
109
+
110
+ # Current table of header key-value pairs.
111
+ attr_reader :table
112
+
113
+ # Current encoding options
114
+ #
115
+ # :table_size Integer maximum dynamic table size in bytes
116
+ # :huffman Symbol :always, :never, :shorter
117
+ # :index Symbol :all, :static, :never
118
+ attr_reader :options
119
+
120
+ # Initializes compression context with appropriate client/server
121
+ # defaults and maximum size of the dynamic table.
122
+ #
123
+ # @param options [Hash] encoding options
124
+ # :table_size Integer maximum dynamic table size in bytes
125
+ # :huffman Symbol :always, :never, :shorter
126
+ # :index Symbol :all, :static, :never
127
+ def initialize(**options)
128
+ default_options = {
129
+ huffman: :shorter,
130
+ index: :all,
131
+ table_size: 4096,
132
+ }
133
+
134
+ @table = []
135
+ @options = default_options.merge(options)
136
+ @limit = @options[:table_size]
137
+ end
138
+
139
+ # Duplicates current compression context
140
+ # @return [Context]
141
+ def dup
142
+ other = Context.new(@options)
143
+
144
+ other.instance_variable_set :@table, @table.dup
145
+ other.instance_variable_set :@limit, @limit
146
+
147
+ return other
148
+ end
149
+
150
+ # Finds an entry in current dynamic table by index.
151
+ # Note that index is zero-based in this module.
152
+ #
153
+ # If the index is greater than the last index in the static table,
154
+ # an entry in the dynamic table is dereferenced.
155
+ #
156
+ # If the index is greater than the last header index, an error is raised.
157
+ #
158
+ # @param index [Integer] zero-based index in the dynamic table.
159
+ # @return [Array] +[key, value]+
160
+ def dereference(index)
161
+ # NOTE: index is zero-based in this module.
162
+ value = STATIC_TABLE[index] || @table[index - STATIC_TABLE.size]
163
+
164
+ raise CompressionError, "Index #{index} too large" unless value
165
+
166
+ return value
167
+ end
168
+
169
+ # Header Block Processing
170
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1
171
+ #
172
+ # @param command [Hash] {type:, name:, value:, index:}
173
+ # @return [Array] +[name, value]+ header field that is added to the decoded header list
174
+ def decode(command)
175
+ emit = nil
176
+
177
+ case command[:type]
178
+ when :changetablesize
179
+ self.table_size = command[:value]
180
+
181
+ when :indexed
182
+ # Indexed Representation
183
+ # An _indexed representation_ entails the following actions:
184
+ # o The header field corresponding to the referenced entry in either
185
+ # the static table or dynamic table is added to the decoded header
186
+ # list.
187
+ idx = command[:name]
188
+
189
+ k, v = dereference(idx)
190
+ emit = [k, v]
191
+
192
+ when :incremental, :noindex, :neverindexed
193
+ # A _literal representation_ that is _not added_ to the dynamic table
194
+ # entails the following action:
195
+ # o The header field is added to the decoded header list.
196
+
197
+ # A _literal representation_ that is _added_ to the dynamic table
198
+ # entails the following actions:
199
+ # o The header field is added to the decoded header list.
200
+ # o The header field is inserted at the beginning of the dynamic table.
201
+
202
+ if command[:name].is_a? Integer
203
+ k, v = dereference(command[:name])
204
+
205
+ command = command.dup
206
+ command[:index] ||= command[:name]
207
+ command[:value] ||= v
208
+ command[:name] = k
209
+ end
210
+
211
+ emit = [command[:name], command[:value]]
212
+
213
+ add_to_table(emit) if command[:type] == :incremental
214
+
215
+ else
216
+ raise CompressionError, "Invalid type: #{command[:type]}"
217
+ end
218
+
219
+ return emit
220
+ end
221
+
222
+ # Plan header compression according to +@options [:index]+
223
+ # :never Do not use dynamic table or static table reference at all.
224
+ # :static Use static table only.
225
+ # :all Use all of them.
226
+ #
227
+ # @param headers [Array] +[[name, value], ...]+
228
+ # @return [Array] array of commands
229
+ def encode(headers)
230
+ commands = []
231
+
232
+ # Literals commands are marked with :noindex when index is not used
233
+ noindex = [:static, :never].include?(@options[:index])
234
+
235
+ headers.each do |field, value|
236
+ command = add_command(field, value)
237
+ command[:type] = :noindex if noindex && command[:type] == :incremental
238
+ commands << command
239
+
240
+ decode(command)
241
+ end
242
+
243
+ return commands
244
+ end
245
+
246
+ # Emits command for a header.
247
+ # Prefer static table over dynamic table.
248
+ # Prefer exact match over name-only match.
249
+ #
250
+ # +@options [:index]+ controls whether to use the dynamic table,
251
+ # static table, or both.
252
+ # :never Do not use dynamic table or static table reference at all.
253
+ # :static Use static table only.
254
+ # :all Use all of them.
255
+ #
256
+ # @param header [Array] +[name, value]+
257
+ # @return [Hash] command
258
+ def add_command(*header)
259
+ exact = nil
260
+ name_only = nil
261
+
262
+ if [:all, :static].include?(@options[:index])
263
+ STATIC_TABLE.each_index do |i|
264
+ if STATIC_TABLE[i] == header
265
+ exact ||= i
266
+ break
267
+ elsif STATIC_TABLE[i].first == header.first
268
+ name_only ||= i
269
+ end
270
+ end
271
+ end
272
+ if [:all].include?(@options[:index]) && !exact
273
+ @table.each_index do |i|
274
+ if @table[i] == header
275
+ exact ||= i + STATIC_TABLE.size
276
+ break
277
+ elsif @table[i].first == header.first
278
+ name_only ||= i + STATIC_TABLE.size
279
+ end
280
+ end
281
+ end
282
+
283
+ if exact
284
+ {name: exact, type: :indexed}
285
+ elsif name_only
286
+ {name: name_only, value: header.last, type: :incremental}
287
+ else
288
+ {name: header.first, value: header.last, type: :incremental}
289
+ end
290
+ end
291
+
292
+ # Alter dynamic table size.
293
+ # When the size is reduced, some headers might be evicted.
294
+ def table_size=(size)
295
+ @limit = size
296
+ size_check(nil)
297
+ end
298
+
299
+ # Returns current table size in octets
300
+ # @return [Integer]
301
+ def current_table_size
302
+ @table.inject(0) {|r, (k, v)| r + k.bytesize + v.bytesize + 32}
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 command [Array] +[name, value]+
312
+ def add_to_table(command)
313
+ return unless size_check(command)
314
+ @table.unshift(command)
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 command [Hash]
321
+ # @return [Boolean] whether +command+ fits in the dynamic table.
322
+ def size_check(command)
323
+ cursize = current_table_size
324
+ cmdsize = command.nil? ? 0 : command[0].bytesize + command[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