Class: Quicsilver::Protocol::Qpack::HeaderBlockDecoder

Inherits:
Object
  • Object
show all
Includes:
Decoder
Defined in:
lib/quicsilver/protocol/qpack/header_block_decoder.rb

Overview

Decodes a QPACK header block into [name, value] pairs.

Default implementation uses the static table only (no dynamic table). To add dynamic table support, implement a class with the same #decode interface:

class MyDynamicDecoder
  def decode(payload)
    # parse QPACK field lines from payload
    # yield [name, value] for each decoded header
  end
end

Then inject it:

RequestParser.new(data, decoder: MyDynamicDecoder.new)
ResponseParser.new(data, decoder: MyDynamicDecoder.new)

Constant Summary collapse

DECODE_CACHE_MAX =
256

Constants included from Decoder

Decoder::DQS_CACHE, Decoder::DQS_CACHE_MAX, Decoder::DQS_LAST_A, Decoder::DQS_LAST_B, Decoder::DQS_OID_CACHE

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Decoder

#decode_prefix_integer, #decode_prefix_integer_str, #decode_qpack_string, #decode_qpack_string_from_str

Constructor Details

#initializeHeaderBlockDecoder

Returns a new instance of HeaderBlockDecoder.



35
36
37
# File 'lib/quicsilver/protocol/qpack/header_block_decoder.rb', line 35

def initialize
  @decode_cache = {}
end

Class Method Details

.defaultObject

Shared default instance for parsers that don’t need custom decoders



31
32
33
# File 'lib/quicsilver/protocol/qpack/header_block_decoder.rb', line 31

def self.default
  @default ||= new
end

Instance Method Details

#decode(payload) ⇒ Object

Decode a QPACK header block payload (RFC 9204 §4.5). Yields [name, value] for each decoded field line.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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
# File 'lib/quicsilver/protocol/qpack/header_block_decoder.rb', line 41

def decode(payload)
  return if payload.nil? || payload.bytesize < 2

  # Check cache for previously decoded payloads
  if payload.bytesize <= 256
    cached = @decode_cache[payload]
    if cached
      cached.each { |name, value| yield name, value }
      return
    end
  end

  headers = []

  # Decode Required Insert Count (RFC 9204 §4.5.1) — 8-bit prefix integer
  required_insert_count, ric_bytes = decode_prefix_integer_str(payload, 0, 8, 0x00)
  offset = ric_bytes

  # Decode Delta Base (RFC 9204 §4.5.1) — 7-bit prefix integer with sign bit
  delta_base, db_bytes = decode_prefix_integer_str(payload, offset, 7, 0x80)
  offset += db_bytes

  # Static-only mode: Required Insert Count and Delta Base must be 0
  if required_insert_count != 0 || delta_base != 0
    raise Protocol::FrameError.new(
      "Dynamic QPACK table not supported (required_insert_count=#{required_insert_count}, delta_base=#{delta_base})",
      error_code: Protocol::QPACK_DECOMPRESSION_FAILED
    )
  end

  while offset < payload.bytesize
    byte = payload.getbyte(offset)

    # Indexed Field Line (1Txxxxxx) — name + value from static table
    if (byte & 0x80) == 0x80
      index, bytes_consumed = decode_prefix_integer_str(payload, offset, 6, 0xC0)
      offset += bytes_consumed

      if index < Protocol::STATIC_TABLE.size
        name, value = Protocol::STATIC_TABLE[index]
        headers << [name, value]
        yield name, value
      else
        raise Protocol::FrameError.new(
          "Invalid QPACK static table index #{index}",
          error_code: Protocol::QPACK_DECOMPRESSION_FAILED
        )
      end

    # Literal with Name Reference (01NTxxxx) — name from static table, literal value
    elsif (byte & 0xC0) == 0x40
      index, bytes_consumed = decode_prefix_integer_str(payload, offset, 4, 0xF0)
      offset += bytes_consumed

      if index >= Protocol::STATIC_TABLE.size
        raise Protocol::FrameError.new(
          "Invalid QPACK static table index #{index}",
          error_code: Protocol::QPACK_DECOMPRESSION_FAILED
        )
      end

      name = Protocol::STATIC_TABLE[index][0]
      value, consumed = decode_qpack_string_from_str(payload, offset)
      offset += consumed
      headers << [name, value]
      yield name, value

    # Literal with Literal Name (001NHxxx) — both name and value are literals
    elsif (byte & 0xE0) == 0x20
      huffman_name = (byte & 0x08) != 0
      name_len, name_len_bytes = decode_prefix_integer_str(payload, offset, 3, 0x28)
      offset += name_len_bytes
      raw_name = payload.byteslice(offset, name_len)
      name = if huffman_name
        Huffman.decode(raw_name) || raw_name
      else
        raw_name
      end
      offset += name_len

      value, consumed = decode_qpack_string_from_str(payload, offset)
      offset += consumed

      headers << [name, value]
      yield name, value
    else
      break
    end
  end

  # Cache the result
  if payload.bytesize <= 256 && @decode_cache.size < DECODE_CACHE_MAX
    key = payload.frozen? ? payload : payload.dup.freeze
    @decode_cache[key] = headers.freeze
  end
end