Class: Protocol::HPACK::Decompressor
- Inherits:
-
Object
- Object
- Protocol::HPACK::Decompressor
- 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
-
#buffer ⇒ Object
readonly
Returns the value of attribute buffer.
-
#context ⇒ Object
readonly
Returns the value of attribute context.
-
#offset ⇒ Object
readonly
Returns the value of attribute offset.
-
#table_size_limit ⇒ Object
readonly
Returns the value of attribute table_size_limit.
Instance Method Summary collapse
-
#decode(list = []) ⇒ Array
Decodes and processes header commands within provided buffer.
- #end? ⇒ Boolean
-
#initialize(buffer, context = Context.new, table_size_limit: nil) ⇒ Decompressor
constructor
A new instance of Decompressor.
- #peek_byte ⇒ Object
- #read_byte ⇒ Object
- #read_bytes(length) ⇒ Object
-
#read_header ⇒ Hash
Decodes header command from provided buffer.
-
#read_integer(bits) ⇒ Integer
Decodes integer value from provided buffer.
-
#read_string ⇒ String
Decodes string value from provided buffer.
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
#buffer ⇒ Object (readonly)
Returns the value of attribute buffer.
28 29 30 |
# File 'lib/protocol/hpack/decompressor.rb', line 28 def buffer @buffer end |
#context ⇒ Object (readonly)
Returns the value of attribute context.
29 30 31 |
# File 'lib/protocol/hpack/decompressor.rb', line 29 def context @context end |
#offset ⇒ Object (readonly)
Returns the value of attribute offset.
30 31 32 |
# File 'lib/protocol/hpack/decompressor.rb', line 30 def offset @offset end |
#table_size_limit ⇒ Object (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.
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
34 35 36 |
# File 'lib/protocol/hpack/decompressor.rb', line 34 def end? @offset >= @buffer.bytesize end |
#peek_byte ⇒ Object
46 47 48 |
# File 'lib/protocol/hpack/decompressor.rb', line 46 def peek_byte @buffer.getbyte(@offset) end |
#read_byte ⇒ Object
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_header ⇒ Hash
Decodes header command from provided buffer.
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.
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_string ⇒ String
Decodes string value from provided buffer.
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 |