Class: Protocol::HPACK::Decompressor

Inherits:
Object
  • Object
show all
Defined in:
lib/protocol/hpack/decompressor.rb

Overview

Responsible for decoding received headers and maintaining compression context of the opposing peer. Decompressor must be initialized with appropriate starting context based on local role: client or server.

Constant Summary collapse

MASK_SHIFT_4 =
(~0x0 >> 4) << 4

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffer, context = Context.new, table_size_limit: nil) ⇒ Decompressor

Returns a new instance of Decompressor.



20
21
22
23
24
25
26
# File 'lib/protocol/hpack/decompressor.rb', line 20

def initialize(buffer, context = Context.new, table_size_limit: nil)
  @buffer = buffer
  @context = context
  @offset = 0
  
  @table_size_limit = table_size_limit
end

Instance Attribute Details

#bufferObject (readonly)

Returns the value of attribute buffer.



28
29
30
# File 'lib/protocol/hpack/decompressor.rb', line 28

def buffer
  @buffer
end

#contextObject (readonly)

Returns the value of attribute context.



29
30
31
# File 'lib/protocol/hpack/decompressor.rb', line 29

def context
  @context
end

#offsetObject (readonly)

Returns the value of attribute offset.



30
31
32
# File 'lib/protocol/hpack/decompressor.rb', line 30

def offset
  @offset
end

#table_size_limitObject (readonly)

Returns the value of attribute table_size_limit.



32
33
34
# File 'lib/protocol/hpack/decompressor.rb', line 32

def table_size_limit
  @table_size_limit
end

Instance Method Details

#decode(list = []) ⇒ Array

Decodes and processes header commands within provided buffer.

Parameters:

  • buffer (Buffer)

Returns:

  • (Array)

    [[name, value], …]



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/protocol/hpack/decompressor.rb', line 192

def decode(list = [])
  while !end?
    command = read_header
    
    if pair = @context.decode(command)
      list << pair
    end
  end
  
  if command and command[:type] == :change_table_size
    raise CompressionError, "Trailing table size update!"
  end
  
  return list
end

#end?Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/protocol/hpack/decompressor.rb', line 34

def end?
  @offset >= @buffer.bytesize
end

#peek_byteObject



46
47
48
# File 'lib/protocol/hpack/decompressor.rb', line 46

def peek_byte
  @buffer.getbyte(@offset)
end

#read_byteObject



38
39
40
41
42
43
44
# File 'lib/protocol/hpack/decompressor.rb', line 38

def read_byte
  if byte = @buffer.getbyte(@offset)
    @offset += 1
  end
  
  return byte
end

#read_bytes(length) ⇒ Object



50
51
52
53
54
55
56
# File 'lib/protocol/hpack/decompressor.rb', line 50

def read_bytes(length)
  slice = @buffer.byteslice(@offset, length)
  
  @offset += length
  
  return slice
end

#read_headerHash

Decodes header command from provided buffer.

Parameters:

  • buffer (Buffer)

Returns:

  • (Hash)

    command



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/protocol/hpack/decompressor.rb', line 102

def read_header
  pattern = peek_byte

  header = {}

  type = nil

  # (pattern & MASK_SHIFT_4) clears bottom 4 bits,
  # equivalent to (pattern >> 4) << 4. For the
  # no-index and never-indexed type we only need to clear
  # the bottom 4 bits (as specified by NO_INDEX_TYPE[:prefix])
  # so we directly check against NO_INDEX_TYPE[:pattern].
  # But for change-table-size, incremental, and indexed
  # we must clear 5,6, and 7 bits respectively.
  # Consider indexed where we need to clear 7 bits.
  # Since (pattern & MASK_SHIFT_4)'s bottom 4 bits are cleared
  # you can visualize it as
  #
  # INDEXED_TYPE[:pattern] = <some bits>      0  0  0  0 0 0 0
  #                                           ^^^^^^^^^^^^^^^^ 7 bits
  # (pattern & MASK_SHIFT_4) = <pattern bits> b1 b2 b3 0 0 0 0
  #
  # Computing equality after masking bottom 7 bits (i.e., set b1 = b2 = b3 = 0)
  # is the same as checking equality against
  #                 <some bits> x1 x2 x3 0 0 0 0
  # For *every* possible value of x1, x2, x3 (that is, 2^3 = 8 values).
  # INDEXED_TYPE[:pattern] = 0x80, so we check against 0x80, 0x90 = 0x80 + (0b001 << 4)
  # 0xa0 = 0x80 + (0b001 << 5), ..., 0xf0 = 0x80 + (0b111 << 4).
  # While not the most readable, we have written out everything as constant literals
  # so Ruby can optimize this case-when to a hash lookup.
  #
  # There's no else case as this list is exhaustive.
  # (0..255).map { |x| (x & -16).to_s(16) }.uniq will show this

  case (pattern & MASK_SHIFT_4)
  when 0x00
    header[:type] = :no_index
    type = NO_INDEX_TYPE
  when 0x10
    header[:type] = :never_indexed
    type = NEVER_INDEXED_TYPE
  # checking if (pattern >> 5) << 5 == 0x20
  # Since we cleared bottom 4 bits, the 5th
  # bit can be either 0 or 1, so check both
  # cases.
  when 0x20, 0x30
    header[:type] = :change_table_size
    type = CHANGE_TABLE_SIZE_TYPE
  # checking if (pattern >> 6) << 6 == 0x40
  # Same logic as above, but now over the 4
  # possible combinations of 2 bits (5th, 6th)
  when 0x40, 0x50, 0x60, 0x70
    header[:type] = :incremental
    type = INCREMENTAL_TYPE
  # checking if (pattern >> 7) << 7 == 0x80
  when 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0
    header[:type] = :indexed
    type = INDEXED_TYPE
  end

  header_name = read_integer(type[:prefix])

  case header[:type]
  when :indexed
    raise CompressionError if header_name.zero?
    header[:name] = header_name - 1
  when :change_table_size
    header[:name] = header_name
    header[:value] = header_name
    
    if @table_size_limit and header[:value] > @table_size_limit
      raise CompressionError, "Table size #{header[:value]} exceeds limit #{@table_size_limit}!"
    end
  else
    if header_name.zero?
      header[:name] = read_string
    else
      header[:name] = header_name - 1
    end
    
    header[:value] = read_string
  end

  return header
end

#read_integer(bits) ⇒ Integer

Decodes integer value from provided buffer.

Parameters:

  • bits (Integer)

    number of available bits

Returns:

  • (Integer)


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/protocol/hpack/decompressor.rb', line 62

def read_integer(bits)
  limit = 2**bits - 1
  value = bits.zero? ? 0 : (read_byte & limit)
  
  shift = 0
  
  while byte = read_byte
    value += ((byte & 127) << shift)
    shift += 7
    
    break if (byte & 128).zero?
  end if (value == limit)
  
  return value
end

#read_stringString

Decodes string value from provided buffer.

Returns:

  • (String)

    UTF-8 encoded string

Raises:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/protocol/hpack/decompressor.rb', line 82

def read_string
  huffman = (peek_byte & 0x80) == 0x80
  
  length = read_integer(7)
  
  raise CompressionError, "Invalid string length!" unless length
  
  string = read_bytes(length)
  
  raise CompressionError, "Invalid string length, got #{string.bytesize}, expecting #{length}!" unless string.bytesize == length
  
  string = Huffman.decode(string) if huffman
  
  return string.force_encoding(Encoding::UTF_8)
end