quicsilver 0.1.0 → 0.3.0
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 +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +76 -5
- data/Gemfile.lock +18 -4
- data/LICENSE +21 -0
- data/README.md +33 -53
- data/Rakefile +29 -2
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +46 -0
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/examples/minimal_http3_server.rb +0 -6
- data/examples/rack_http3_server.rb +0 -6
- data/examples/simple_client_test.rb +26 -0
- data/ext/quicsilver/quicsilver.c +615 -138
- data/lib/quicsilver/client/client.rb +250 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/protocol/frames.rb +327 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +387 -0
- data/lib/quicsilver/protocol/response_encoder.rb +72 -0
- data/lib/quicsilver/protocol/response_parser.rb +249 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +86 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +336 -0
- data/lib/quicsilver/transport/configuration.rb +132 -0
- data/lib/quicsilver/transport/connection.rb +350 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +49 -9
- data/lib/rackup/handler/quicsilver.rb +77 -0
- data/quicsilver.gemspec +10 -3
- metadata +122 -17
- data/examples/minimal_http3_client.rb +0 -89
- data/lib/quicsilver/client.rb +0 -191
- data/lib/quicsilver/http3/request_encoder.rb +0 -112
- data/lib/quicsilver/http3/request_parser.rb +0 -158
- data/lib/quicsilver/http3/response_encoder.rb +0 -73
- data/lib/quicsilver/http3.rb +0 -68
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/server.rb +0 -258
- data/lib/quicsilver/server_configuration.rb +0 -49
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "huffman"
|
|
4
|
+
|
|
5
|
+
module Quicsilver
|
|
6
|
+
module Protocol
|
|
7
|
+
module Qpack
|
|
8
|
+
module Decoder
|
|
9
|
+
# Decode a QPACK string literal (RFC 9204 Section 4.1.2)
|
|
10
|
+
# Returns [string, bytes_consumed]
|
|
11
|
+
# String-based variant: accepts a binary String instead of byte array
|
|
12
|
+
def decode_qpack_string_from_str(data, offset)
|
|
13
|
+
first = data.getbyte(offset)
|
|
14
|
+
huffman = (first & 0x80) != 0
|
|
15
|
+
|
|
16
|
+
length = first & 0x7F
|
|
17
|
+
len_bytes = 1
|
|
18
|
+
if length == 0x7F
|
|
19
|
+
multiplier = 1
|
|
20
|
+
while offset + len_bytes < data.bytesize
|
|
21
|
+
next_byte = data.getbyte(offset + len_bytes)
|
|
22
|
+
len_bytes += 1
|
|
23
|
+
length += (next_byte & 0x7F) * multiplier
|
|
24
|
+
break if (next_byte & 0x80) == 0
|
|
25
|
+
multiplier *= 128
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
data_offset = offset + len_bytes
|
|
30
|
+
raw = data.byteslice(data_offset, length)
|
|
31
|
+
|
|
32
|
+
str = if huffman
|
|
33
|
+
Huffman.decode(raw) || raw
|
|
34
|
+
else
|
|
35
|
+
raw
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[str, len_bytes + length]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Cache for decode_qpack_string
|
|
42
|
+
DQS_CACHE = {} # array-content → [str, consumed]
|
|
43
|
+
DQS_OID_CACHE = {} # object_id|offset → [str, consumed]
|
|
44
|
+
DQS_CACHE_MAX = 128
|
|
45
|
+
|
|
46
|
+
# 2-slot last-result cache for decode_qpack_string
|
|
47
|
+
DQS_LAST_A = [nil, nil, nil] # [bytes, offset, result]
|
|
48
|
+
DQS_LAST_B = [nil, nil, nil]
|
|
49
|
+
|
|
50
|
+
def decode_qpack_string(bytes, offset)
|
|
51
|
+
# 2-slot equal? fast path (covers alternating-object patterns)
|
|
52
|
+
return DQS_LAST_A[2] if bytes.equal?(DQS_LAST_A[0]) && offset == DQS_LAST_A[1]
|
|
53
|
+
return DQS_LAST_B[2] if bytes.equal?(DQS_LAST_B[0]) && offset == DQS_LAST_B[1]
|
|
54
|
+
|
|
55
|
+
# Object-id cache
|
|
56
|
+
oid_key = (bytes.object_id << 16) | offset
|
|
57
|
+
cached = DQS_OID_CACHE[oid_key]
|
|
58
|
+
if cached
|
|
59
|
+
# Rotate 2-slot cache
|
|
60
|
+
DQS_LAST_B[0], DQS_LAST_B[1], DQS_LAST_B[2] = DQS_LAST_A[0], DQS_LAST_A[1], DQS_LAST_A[2]
|
|
61
|
+
DQS_LAST_A[0], DQS_LAST_A[1], DQS_LAST_A[2] = bytes, offset, cached
|
|
62
|
+
return cached
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Dispatch to string variant if given a String
|
|
66
|
+
return decode_qpack_string_from_str(bytes, offset) if bytes.is_a?(String)
|
|
67
|
+
|
|
68
|
+
# Content-based cache for offset=0
|
|
69
|
+
if offset == 0
|
|
70
|
+
cached = DQS_CACHE[bytes]
|
|
71
|
+
if cached
|
|
72
|
+
DQS_OID_CACHE[oid_key] = cached
|
|
73
|
+
return cached
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
first = bytes[offset]
|
|
78
|
+
huffman = (first & 0x80) != 0
|
|
79
|
+
|
|
80
|
+
# Inline 7-bit prefix integer decode to avoid method call
|
|
81
|
+
length = first & 0x7F
|
|
82
|
+
len_bytes = 1
|
|
83
|
+
if length == 0x7F
|
|
84
|
+
multiplier = 1
|
|
85
|
+
while offset + len_bytes < bytes.size
|
|
86
|
+
next_byte = bytes[offset + len_bytes]
|
|
87
|
+
len_bytes += 1
|
|
88
|
+
length += (next_byte & 0x7F) * multiplier
|
|
89
|
+
break if (next_byte & 0x80) == 0
|
|
90
|
+
multiplier *= 128
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
data_offset = offset + len_bytes
|
|
95
|
+
raw = bytes[data_offset, length].pack("C*")
|
|
96
|
+
|
|
97
|
+
str = if huffman
|
|
98
|
+
Huffman.decode(raw) || raw
|
|
99
|
+
else
|
|
100
|
+
raw
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
result = [str, len_bytes + length].freeze
|
|
104
|
+
|
|
105
|
+
# Cache for offset=0 (common case: standalone decode)
|
|
106
|
+
if offset == 0 && DQS_CACHE.size < DQS_CACHE_MAX
|
|
107
|
+
DQS_CACHE[bytes.frozen? ? bytes : bytes.dup.freeze] = result
|
|
108
|
+
end
|
|
109
|
+
DQS_OID_CACHE[oid_key] = result if DQS_OID_CACHE.size < DQS_CACHE_MAX
|
|
110
|
+
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# String-based prefix integer decoding
|
|
115
|
+
def decode_prefix_integer_str(data, offset, prefix_bits, pattern_mask)
|
|
116
|
+
max_prefix = (1 << prefix_bits) - 1
|
|
117
|
+
first_byte = data.getbyte(offset)
|
|
118
|
+
value = first_byte & max_prefix
|
|
119
|
+
bytes_consumed = 1
|
|
120
|
+
|
|
121
|
+
if value == max_prefix
|
|
122
|
+
multiplier = 1
|
|
123
|
+
loop do
|
|
124
|
+
return [value, bytes_consumed] if offset + bytes_consumed >= data.bytesize
|
|
125
|
+
next_byte = data.getbyte(offset + bytes_consumed)
|
|
126
|
+
bytes_consumed += 1
|
|
127
|
+
value += (next_byte & 0x7F) * multiplier
|
|
128
|
+
break if (next_byte & 0x80) == 0
|
|
129
|
+
multiplier *= 128
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
[value, bytes_consumed]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# RFC 7541 prefix integer decoding
|
|
137
|
+
# Returns [value, bytes_consumed]
|
|
138
|
+
def decode_prefix_integer(bytes, offset, prefix_bits, pattern_mask)
|
|
139
|
+
max_prefix = (1 << prefix_bits) - 1
|
|
140
|
+
|
|
141
|
+
first_byte = bytes[offset]
|
|
142
|
+
value = first_byte & max_prefix
|
|
143
|
+
bytes_consumed = 1
|
|
144
|
+
|
|
145
|
+
if value == max_prefix
|
|
146
|
+
multiplier = 1
|
|
147
|
+
loop do
|
|
148
|
+
return [value, bytes_consumed] if offset + bytes_consumed >= bytes.size
|
|
149
|
+
|
|
150
|
+
next_byte = bytes[offset + bytes_consumed]
|
|
151
|
+
bytes_consumed += 1
|
|
152
|
+
|
|
153
|
+
value += (next_byte & 0x7F) * multiplier
|
|
154
|
+
break if (next_byte & 0x80) == 0
|
|
155
|
+
|
|
156
|
+
multiplier *= 128
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
[value, bytes_consumed]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "huffman"
|
|
3
|
+
|
|
4
|
+
module Quicsilver
|
|
5
|
+
module Protocol
|
|
6
|
+
module Qpack
|
|
7
|
+
class Encoder
|
|
8
|
+
STATIC_TABLE = Protocol::STATIC_TABLE
|
|
9
|
+
|
|
10
|
+
# Pre-built hash for O(1) static table lookups
|
|
11
|
+
STATIC_LOOKUP_FULL = {} # "name\0value" => index
|
|
12
|
+
STATIC_LOOKUP_NAME = {} # name => first_index
|
|
13
|
+
|
|
14
|
+
STATIC_TABLE.each_with_index do |(tbl_name, tbl_value), idx|
|
|
15
|
+
STATIC_LOOKUP_FULL["#{tbl_name}\0#{tbl_value}".freeze] = idx
|
|
16
|
+
STATIC_LOOKUP_NAME[tbl_name] ||= idx
|
|
17
|
+
end
|
|
18
|
+
STATIC_LOOKUP_FULL.freeze
|
|
19
|
+
STATIC_LOOKUP_NAME.freeze
|
|
20
|
+
|
|
21
|
+
PREFIX = "\x00\x00".b.freeze
|
|
22
|
+
|
|
23
|
+
FIELD_CACHE_MAX = 512
|
|
24
|
+
|
|
25
|
+
def initialize(huffman: true)
|
|
26
|
+
@huffman = huffman
|
|
27
|
+
@field_cache = {}
|
|
28
|
+
@block_cache = {}
|
|
29
|
+
@oid_cache = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def encode(headers)
|
|
33
|
+
# Fastest path: exact same object as last call
|
|
34
|
+
return @last_result if headers.equal?(@last_headers)
|
|
35
|
+
|
|
36
|
+
# Fast path: check object_id cache (same array object reused)
|
|
37
|
+
oid = headers.object_id
|
|
38
|
+
cached = @oid_cache[oid]
|
|
39
|
+
if cached
|
|
40
|
+
@last_headers = headers
|
|
41
|
+
@last_result = cached
|
|
42
|
+
return cached
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if headers.is_a?(Array) && headers.size <= 16
|
|
46
|
+
# Content-based caching for small header sets
|
|
47
|
+
block_key = headers.map { |n, v| "#{n}\0#{v}" }.join("\x01")
|
|
48
|
+
cached_block = @block_cache[block_key]
|
|
49
|
+
if cached_block
|
|
50
|
+
@oid_cache[oid] = cached_block if @oid_cache.size < BLOCK_CACHE_MAX
|
|
51
|
+
return cached_block
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = encode_fields(headers)
|
|
55
|
+
result_frozen = result.freeze
|
|
56
|
+
if @block_cache.size < BLOCK_CACHE_MAX
|
|
57
|
+
@block_cache[block_key.freeze] = result_frozen
|
|
58
|
+
end
|
|
59
|
+
@oid_cache[oid] = result_frozen if @oid_cache.size < BLOCK_CACHE_MAX
|
|
60
|
+
return result_frozen
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
encode_fields(headers)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
BLOCK_CACHE_MAX = 128
|
|
67
|
+
|
|
68
|
+
private def encode_fields(headers)
|
|
69
|
+
out = encode_prefix
|
|
70
|
+
headers.each do |name, value|
|
|
71
|
+
name = name.to_s
|
|
72
|
+
value = value.to_s
|
|
73
|
+
# Downcase only if needed (most HTTP/3 headers are already lowercase)
|
|
74
|
+
name = name.downcase if name =~ /[A-Z]/
|
|
75
|
+
|
|
76
|
+
cache_key = "#{name}\0#{value}"
|
|
77
|
+
|
|
78
|
+
# Check field cache
|
|
79
|
+
cached = @field_cache[cache_key]
|
|
80
|
+
if cached
|
|
81
|
+
out << cached
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
field_start = out.bytesize
|
|
86
|
+
|
|
87
|
+
full_idx = STATIC_LOOKUP_FULL[cache_key]
|
|
88
|
+
|
|
89
|
+
if full_idx
|
|
90
|
+
# Indexed Field Line
|
|
91
|
+
out << encode_prefixed_int(full_idx, 6, 0xC0)
|
|
92
|
+
else
|
|
93
|
+
name_idx = STATIC_LOOKUP_NAME[name]
|
|
94
|
+
if name_idx
|
|
95
|
+
# Literal with Name Reference
|
|
96
|
+
out << encode_prefixed_int(name_idx, 4, 0x50)
|
|
97
|
+
encode_str_into(out, value)
|
|
98
|
+
else
|
|
99
|
+
# Literal with Literal Name
|
|
100
|
+
encode_literal_into(out, name, value)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Cache the encoded field bytes
|
|
105
|
+
if @field_cache.size < FIELD_CACHE_MAX
|
|
106
|
+
@field_cache[cache_key.freeze] = out.byteslice(field_start..).freeze
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
out
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def lookup(name, value)
|
|
113
|
+
name = name.to_s.downcase
|
|
114
|
+
value = value.to_s
|
|
115
|
+
full_idx = STATIC_LOOKUP_FULL["#{name}\0#{value}"]
|
|
116
|
+
return [full_idx, true] if full_idx
|
|
117
|
+
name_idx = STATIC_LOOKUP_NAME[name]
|
|
118
|
+
name_idx ? [name_idx, false] : nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def encode_prefix
|
|
122
|
+
PREFIX.dup
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Public API for encoding a single string value (used by tests/external code)
|
|
126
|
+
def encode_str(value)
|
|
127
|
+
out = "".b
|
|
128
|
+
encode_str_into(out, value.to_s)
|
|
129
|
+
out
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Pattern 1: Indexed Field Line (1xxxxxxx) — kept for test compatibility
|
|
135
|
+
def encode_indexed(index)
|
|
136
|
+
encode_prefixed_int(index, 6, 0xC0)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def encode_literal_into(out, name, value)
|
|
140
|
+
name_b = name.b
|
|
141
|
+
if @huffman
|
|
142
|
+
huffman_name = Huffman.encode(name_b)
|
|
143
|
+
if huffman_name.bytesize < name_b.bytesize
|
|
144
|
+
out << encode_prefixed_int(huffman_name.bytesize, 3, 0x28)
|
|
145
|
+
out << huffman_name
|
|
146
|
+
encode_str_into(out, value)
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
out << encode_prefixed_int(name_b.bytesize, 3, 0x20)
|
|
151
|
+
out << name_b
|
|
152
|
+
encode_str_into(out, value)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def encode_str_into(out, value)
|
|
156
|
+
value_b = value.b
|
|
157
|
+
if @huffman
|
|
158
|
+
huffman = Huffman.encode(value_b)
|
|
159
|
+
if huffman.bytesize < value_b.bytesize
|
|
160
|
+
out << encode_prefixed_int(huffman.bytesize, 7, 0x80)
|
|
161
|
+
out << huffman
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
out << encode_prefixed_int(value_b.bytesize, 7, 0x00)
|
|
166
|
+
out << value_b
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# RFC 7541 prefix integer encoding
|
|
170
|
+
def encode_prefixed_int(value, prefix_bits, pattern)
|
|
171
|
+
max_prefix = (1 << prefix_bits) - 1
|
|
172
|
+
|
|
173
|
+
if value < max_prefix
|
|
174
|
+
(pattern | value).chr(Encoding::BINARY)
|
|
175
|
+
else
|
|
176
|
+
buf = (pattern | max_prefix).chr(Encoding::BINARY)
|
|
177
|
+
value -= max_prefix
|
|
178
|
+
while value >= 128
|
|
179
|
+
buf << ((value & 0x7F) | 0x80).chr(Encoding::BINARY)
|
|
180
|
+
value >>= 7
|
|
181
|
+
end
|
|
182
|
+
buf << value.chr(Encoding::BINARY)
|
|
183
|
+
buf
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "decoder"
|
|
4
|
+
require_relative "huffman"
|
|
5
|
+
|
|
6
|
+
module Quicsilver
|
|
7
|
+
module Protocol
|
|
8
|
+
module Qpack
|
|
9
|
+
# Decodes a QPACK header block into [name, value] pairs.
|
|
10
|
+
#
|
|
11
|
+
# Default implementation uses the static table only (no dynamic table).
|
|
12
|
+
# To add dynamic table support, implement a class with the same #decode interface:
|
|
13
|
+
#
|
|
14
|
+
# class MyDynamicDecoder
|
|
15
|
+
# def decode(payload)
|
|
16
|
+
# # parse QPACK field lines from payload
|
|
17
|
+
# # yield [name, value] for each decoded header
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Then inject it:
|
|
22
|
+
# RequestParser.new(data, decoder: MyDynamicDecoder.new)
|
|
23
|
+
# ResponseParser.new(data, decoder: MyDynamicDecoder.new)
|
|
24
|
+
#
|
|
25
|
+
class HeaderBlockDecoder
|
|
26
|
+
include Decoder
|
|
27
|
+
|
|
28
|
+
DECODE_CACHE_MAX = 256
|
|
29
|
+
|
|
30
|
+
# Shared default instance for parsers that don't need custom decoders
|
|
31
|
+
def self.default
|
|
32
|
+
@default ||= new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@decode_cache = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Decode a QPACK header block payload (RFC 9204 §4.5).
|
|
40
|
+
# Yields [name, value] for each decoded field line.
|
|
41
|
+
def decode(payload)
|
|
42
|
+
return if payload.nil? || payload.bytesize < 2
|
|
43
|
+
|
|
44
|
+
# Check cache for previously decoded payloads
|
|
45
|
+
if payload.bytesize <= 256
|
|
46
|
+
cached = @decode_cache[payload]
|
|
47
|
+
if cached
|
|
48
|
+
cached.each { |name, value| yield name, value }
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
headers = []
|
|
54
|
+
offset = 2 # skip required insert count + delta base prefix
|
|
55
|
+
|
|
56
|
+
while offset < payload.bytesize
|
|
57
|
+
byte = payload.getbyte(offset)
|
|
58
|
+
|
|
59
|
+
# Indexed Field Line (1Txxxxxx) — name + value from static table
|
|
60
|
+
if (byte & 0x80) == 0x80
|
|
61
|
+
index, bytes_consumed = decode_prefix_integer_str(payload, offset, 6, 0xC0)
|
|
62
|
+
offset += bytes_consumed
|
|
63
|
+
|
|
64
|
+
if index < Protocol::STATIC_TABLE.size
|
|
65
|
+
name, value = Protocol::STATIC_TABLE[index]
|
|
66
|
+
headers << [name, value]
|
|
67
|
+
yield name, value
|
|
68
|
+
else
|
|
69
|
+
raise Protocol::FrameError.new(
|
|
70
|
+
"Invalid QPACK static table index #{index}",
|
|
71
|
+
error_code: Protocol::QPACK_DECOMPRESSION_FAILED
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Literal with Name Reference (01NTxxxx) — name from static table, literal value
|
|
76
|
+
elsif (byte & 0xC0) == 0x40
|
|
77
|
+
index, bytes_consumed = decode_prefix_integer_str(payload, offset, 4, 0xF0)
|
|
78
|
+
offset += bytes_consumed
|
|
79
|
+
|
|
80
|
+
if index >= Protocol::STATIC_TABLE.size
|
|
81
|
+
raise Protocol::FrameError.new(
|
|
82
|
+
"Invalid QPACK static table index #{index}",
|
|
83
|
+
error_code: Protocol::QPACK_DECOMPRESSION_FAILED
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
name = Protocol::STATIC_TABLE[index][0]
|
|
88
|
+
value, consumed = decode_qpack_string_from_str(payload, offset)
|
|
89
|
+
offset += consumed
|
|
90
|
+
headers << [name, value]
|
|
91
|
+
yield name, value
|
|
92
|
+
|
|
93
|
+
# Literal with Literal Name (001NHxxx) — both name and value are literals
|
|
94
|
+
elsif (byte & 0xE0) == 0x20
|
|
95
|
+
huffman_name = (byte & 0x08) != 0
|
|
96
|
+
name_len, name_len_bytes = decode_prefix_integer_str(payload, offset, 3, 0x28)
|
|
97
|
+
offset += name_len_bytes
|
|
98
|
+
raw_name = payload.byteslice(offset, name_len)
|
|
99
|
+
name = if huffman_name
|
|
100
|
+
Huffman.decode(raw_name) || raw_name
|
|
101
|
+
else
|
|
102
|
+
raw_name
|
|
103
|
+
end
|
|
104
|
+
offset += name_len
|
|
105
|
+
|
|
106
|
+
value, consumed = decode_qpack_string_from_str(payload, offset)
|
|
107
|
+
offset += consumed
|
|
108
|
+
|
|
109
|
+
headers << [name, value]
|
|
110
|
+
yield name, value
|
|
111
|
+
else
|
|
112
|
+
break
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Cache the result
|
|
117
|
+
if payload.bytesize <= 256 && @decode_cache.size < DECODE_CACHE_MAX
|
|
118
|
+
key = payload.frozen? ? payload : payload.dup.freeze
|
|
119
|
+
@decode_cache[key] = headers.freeze
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|