protocol-hpack 1.4.3 → 1.5.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3192190834a35550921cbff8e50482499716791371f521a77c3f7c4b5a0efcff
4
- data.tar.gz: 40d443b19eaac26aaaaae63da3baab5ae384246804643fdc7fb01f5455595dc3
3
+ metadata.gz: ad81d71708c7c92bb94e9616655872045a31052aa62c3543487064ddea8c99ad
4
+ data.tar.gz: 1945361f8e8576ef3db44de1e0cf44554795699d910c40b8659232a499ded6b8
5
5
  SHA512:
6
- metadata.gz: c19a7524c1a544016e0feb7d88099a67fbccf3fe200214f7bc21639512da55e72a9e3442c8917f69fec1568a3027487e701a2f1e99deb652942ebae54c4752a7
7
- data.tar.gz: 3f105d7bba7111585325db6bc8f414c34366bfb189f2263fa497e8ec53ed1502c3694c0dcd6749151fb10456c52a5a333daa9b7f5f8b362026d8be745c568375
6
+ metadata.gz: c0537372d9278691fa9621622b84f32facde4f9efdfbfd0c36eb7573bba4b19ec05bc91f5191a7463405cbed952c4c6ac614531c4c0fc0b26707196db8d800b3
7
+ data.tar.gz: fc53196d54e9b02a56df5c1294e819f5a4002c49201811c12d2666725a0325e27df75603afc4b04a5fc5c1845237937097bf51352ea4d5f1c3741d1b73fa244a
checksums.yaml.gz.sig CHANGED
Binary file
@@ -11,9 +11,10 @@
11
11
  # Copyright, 2018, by Kenichi Nakamura.
12
12
  # Copyright, 2019, by Jingyi Chen.
13
13
  # Copyright, 2020, by Justin Mazzocchi.
14
+ # Copyright, 2024, by Nathan Froyd.
14
15
 
15
- require_relative 'context'
16
- require_relative 'huffman'
16
+ require_relative "context"
17
+ require_relative "huffman"
17
18
 
18
19
  module Protocol
19
20
  module HPACK
@@ -74,22 +75,19 @@ module Protocol
74
75
  # @param bits [Integer] number of available bits
75
76
  # @return [String] binary string
76
77
  def write_integer(value, bits)
77
- limit = 2**bits - 1
78
+ limit = (1 << bits) - 1
78
79
 
79
- return write_bytes([value].pack('C')) if value < limit
80
+ return @buffer << value if value < limit
80
81
 
81
- bytes = []
82
- bytes.push(limit) unless bits.zero?
82
+ @buffer << limit unless bits.zero?
83
83
 
84
84
  value -= limit
85
85
  while value >= 128
86
- bytes.push((value % 128) + 128)
86
+ @buffer << ((value & 0x7f) + 128)
87
87
  value /= 128
88
88
  end
89
89
 
90
- bytes.push(value)
91
-
92
- write_bytes(bytes.pack('C*'))
90
+ @buffer << value
93
91
  end
94
92
 
95
93
  def huffman
@@ -118,7 +116,7 @@ module Protocol
118
116
  # @return [String] binary string
119
117
  def write_string(string, huffman = self.huffman)
120
118
  if huffman != :never
121
- encoded = Huffman.new.encode(string)
119
+ encoded = Huffman.encode(string)
122
120
 
123
121
  if huffman == :shorter and encoded.bytesize >= string.bytesize
124
122
  encoded = nil
@@ -2,8 +2,10 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
+ # Copyright, 2024, by Maruth Goyal.
6
+ # Copyright, 2024, by Nathan Froyd.
5
7
 
6
- require_relative 'huffman'
8
+ require_relative "huffman"
7
9
 
8
10
  module Protocol
9
11
  # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
@@ -94,7 +96,20 @@ module Protocol
94
96
  ["via", ""],
95
97
  ["www-authenticate", ""],
96
98
  ].each(&:freeze).freeze
97
-
99
+
100
+ STATIC_EXACT_LOOKUP = {}
101
+ STATIC_NAME_LOOKUP = {}
102
+
103
+ STATIC_TABLE.each_with_index do |(name, value), i|
104
+ exact_header_values = (STATIC_EXACT_LOOKUP[name] ||= [])
105
+ exact_header_values << [value, i]
106
+ STATIC_NAME_LOOKUP[name] = i if STATIC_NAME_LOOKUP[name].nil?
107
+ end
108
+
109
+ STATIC_EXACT_LOOKUP.each {|k, v| v.freeze}
110
+ STATIC_EXACT_LOOKUP.freeze
111
+ STATIC_NAME_LOOKUP.freeze
112
+
98
113
  # Initializes compression context with appropriate client/server defaults and maximum size of the dynamic table.
99
114
  #
100
115
  # @param table [Array] Table of header key-value pairs.
@@ -233,29 +248,42 @@ module Protocol
233
248
  # :static Use static table only.
234
249
  # :all Use all of them.
235
250
  #
236
- # @param header [Array] +[name, value]+
251
+ # @param name [String]
252
+ # @param value [String]
237
253
  # @return [Hash] command
238
- def add_command(*header)
254
+ def add_command(name, value)
239
255
  exact = nil
240
256
  name_only = nil
241
257
 
242
- if [:all, :static].include?(@index)
243
- STATIC_TABLE.each_index do |i|
244
- if STATIC_TABLE[i] == header
245
- exact ||= i
246
- break
247
- elsif STATIC_TABLE[i].first == header.first
248
- name_only ||= i
258
+ if @index == :all || @index == :static
259
+ if (values_and_indices = STATIC_EXACT_LOOKUP[name])
260
+ values_and_indices.each do |known_value, index|
261
+ if value == known_value
262
+ exact = index
263
+ break
264
+ end
249
265
  end
266
+
267
+ needs_name_lookup = exact.nil?
268
+ else
269
+ needs_name_lookup = true
270
+ end
271
+
272
+ if needs_name_lookup && (static_value = STATIC_NAME_LOOKUP[name])
273
+ name_only = static_value
250
274
  end
251
275
  end
252
- if [:all].include?(@index) && !exact
276
+
277
+ if @index == :all && !exact
253
278
  @table.each_index do |i|
254
- if @table[i] == header
255
- exact ||= i + STATIC_TABLE.size
256
- break
257
- elsif @table[i].first == header.first
258
- name_only ||= i + STATIC_TABLE.size
279
+ entry = @table[i]
280
+ if entry.first == name
281
+ if entry.last == value
282
+ exact ||= i + STATIC_TABLE.size
283
+ break
284
+ else
285
+ name_only ||= i + STATIC_TABLE.size
286
+ end
259
287
  end
260
288
  end
261
289
  end
@@ -263,9 +291,9 @@ module Protocol
263
291
  if exact
264
292
  {name: exact, type: :indexed}
265
293
  elsif name_only
266
- {name: name_only, value: header.last, type: :incremental}
294
+ {name: name_only, value: value, type: :incremental}
267
295
  else
268
- {name: header.first, value: header.last, type: :incremental}
296
+ {name: name, value: value, type: :incremental}
269
297
  end
270
298
  end
271
299
 
@@ -2,9 +2,11 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2024, by Samuel Williams.
5
+ # Copyright, 2024, by Maruth Goyal.
6
+ # Copyright, 2024, by Nathan Froyd.
5
7
 
6
- require_relative 'context'
7
- require_relative 'huffman'
8
+ require_relative "context"
9
+ require_relative "huffman"
8
10
 
9
11
  module Protocol
10
12
  module HPACK
@@ -88,7 +90,7 @@ module Protocol
88
90
 
89
91
  raise CompressionError, "Invalid string length, got #{string.bytesize}, expecting #{length}!" unless string.bytesize == length
90
92
 
91
- string = Huffman.new.decode(string) if huffman
93
+ string = Huffman.decode(string) if huffman
92
94
 
93
95
  return string.force_encoding(Encoding::UTF_8)
94
96
  end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2014, by Kaoru Maeda.
5
+ # Copyright, 2015, by Tamir Duberstein.
6
+ # Copyright, 2015, by Ilya Grigorik.
7
+ # Copyright, 2016, by George Ulmer.
8
+ # Copyright, 2018-2024, by Samuel Williams.
9
+ # Copyright, 2024, by Nathan Froyd.
10
+
11
+ require_relative "../huffman"
12
+ require "set"
13
+
14
+ module Protocol
15
+ module HPACK
16
+ class Huffman
17
+ module Generator
18
+ EOS = 256
19
+
20
+ class Node
21
+ attr_accessor :next, :emit, :final, :depth
22
+ attr_accessor :transitions
23
+ attr_accessor :id
24
+
25
+ @@id = 0
26
+
27
+ def initialize(depth)
28
+ @next = [nil, nil]
29
+ @id = @@id
30
+ @@id += 1
31
+ @final = false
32
+ @depth = depth
33
+ end
34
+
35
+ def add(code, len, chr)
36
+ self.final = true if chr == EOS && @depth <= 7
37
+ if len.zero?
38
+ @emit = chr
39
+ else
40
+ bit = (code & (1 << (len - 1))).zero? ? 0 : 1
41
+ node = @next[bit] ||= Node.new(@depth + 1)
42
+ node.add(code, len - 1, chr)
43
+ end
44
+ end
45
+
46
+ class Transition
47
+ attr_accessor :emit, :node
48
+ def initialize(emit, node)
49
+ @emit = emit
50
+ @node = node
51
+ end
52
+ end
53
+
54
+ def self.generate_tree
55
+ @root = new(0)
56
+ Protocol::HPACK::Huffman::CODES.each_with_index do |c, chr|
57
+ code, len = c
58
+ @root.add(code, len, chr)
59
+ end
60
+ puts "#{@@id} nodes"
61
+ @root
62
+ end
63
+
64
+ def self.generate_machine
65
+ generate_tree
66
+
67
+ # Using un-ordered sets (potentially) produces non-deterministic results:
68
+ togo = Set[@root]
69
+ @states = Set[@root]
70
+
71
+ until togo.empty?
72
+ node = togo.first
73
+ togo.delete(node)
74
+
75
+ next if node.transitions
76
+ node.transitions = Array[1 << BITS_AT_ONCE]
77
+
78
+ (1 << BITS_AT_ONCE).times do |input|
79
+ n = node
80
+ emit = +""
81
+ (BITS_AT_ONCE - 1).downto(0) do |i|
82
+ bit = (input & (1 << i)).zero? ? 0 : 1
83
+ n = n.next[bit]
84
+ next unless n.emit
85
+ if n.emit == EOS
86
+ emit = EOS # cause error on decoding
87
+ else
88
+ emit << n.emit.chr(Encoding::BINARY) unless emit == EOS
89
+ end
90
+ n = @root
91
+ end
92
+ node.transitions[input] = Transition.new(emit, n)
93
+ togo << n
94
+ @states << n
95
+ end
96
+ end
97
+ puts "#{@states.size} states"
98
+ @root
99
+ end
100
+
101
+ MACHINE_PATH = File.expand_path("machine.rb", __dir__)
102
+
103
+ def self.generate_state_table(output_path = MACHINE_PATH)
104
+ generate_machine
105
+ state_id = {}
106
+ id_state = {}
107
+ state_id[@root] = 0
108
+ id_state[0] = @root
109
+ max_final = 0
110
+ id = 1
111
+ (@states - [@root]).sort_by {|s| s.final ? 0 : 1}.each do |s|
112
+ state_id[s] = id
113
+ id_state[id] = s
114
+ max_final = id if s.final
115
+ id += 1
116
+ end
117
+
118
+ File.open(output_path, "w") do |file|
119
+ file.print <<~HEADER
120
+ # frozen_string_literal: true
121
+
122
+ # Released under the MIT License.
123
+ # Copyright, 2018-2024, by Samuel Williams.
124
+
125
+ # Machine generated Huffman decoder state machine.
126
+ # DO NOT EDIT THIS FILE.
127
+
128
+ module Protocol
129
+ module HPACK
130
+ class Huffman
131
+ # :nodoc:
132
+ MAX_FINAL_STATE = #{max_final}
133
+ MACHINE = [
134
+ HEADER
135
+
136
+ id.times do |i|
137
+ n = id_state[i]
138
+ file.print "\t\t\t\t["
139
+ string = (1 << BITS_AT_ONCE).times.map do |t|
140
+ transition = n.transitions.fetch(t)
141
+ emit = transition.emit
142
+ unless emit == EOS
143
+ bytes = emit.bytes
144
+ fail ArgumentError if bytes.size > 1
145
+ emit = bytes.first
146
+ end
147
+ "[#{emit.inspect}, #{state_id.fetch(transition.node)}]"
148
+ end.join(", ")
149
+ file.print(string)
150
+ file.print "],\n"
151
+ end
152
+
153
+ file.print <<~FOOTER
154
+ ].each {|arr| arr.each {|subarr| subarr.each(&:freeze)}.freeze}.freeze
155
+ end
156
+ end
157
+ end
158
+ FOOTER
159
+ end
160
+ end
161
+
162
+ class << self
163
+ attr_reader :root
164
+ end
165
+
166
+ # Test decoder
167
+ def self.decode(input)
168
+ emit = ""
169
+ n = root
170
+ nibbles = input.unpack("C*").flat_map {|b| [((b & 0xf0) >> 4), b & 0xf]}
171
+ until nibbles.empty?
172
+ nb = nibbles.shift
173
+ t = n.transitions[nb]
174
+ emit << t.emit
175
+ n = t.node
176
+ end
177
+ unless n.final && nibbles.all? {|x| x == 0xf}
178
+ puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
179
+ end
180
+ emit
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end