quicsilver 0.2.0 → 0.4.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -5
  3. data/.github/workflows/cibuildgem.yaml +93 -0
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +81 -0
  6. data/Gemfile.lock +26 -4
  7. data/README.md +95 -31
  8. data/Rakefile +95 -3
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +1 -1
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/README.md +44 -91
  16. data/examples/benchmark.rb +111 -0
  17. data/examples/connection_pool_demo.rb +47 -0
  18. data/examples/example_helper.rb +18 -0
  19. data/examples/falcon_middleware.rb +44 -0
  20. data/examples/feature_demo.rb +125 -0
  21. data/examples/grpc_style.rb +97 -0
  22. data/examples/minimal_http3_server.rb +6 -18
  23. data/examples/priorities.rb +60 -0
  24. data/examples/protocol_http_server.rb +31 -0
  25. data/examples/rack_http3_server.rb +8 -20
  26. data/examples/rails_feature_test.rb +260 -0
  27. data/examples/simple_client_test.rb +2 -2
  28. data/examples/streaming_sse.rb +33 -0
  29. data/examples/trailers.rb +69 -0
  30. data/ext/quicsilver/extconf.rb +14 -0
  31. data/ext/quicsilver/quicsilver.c +568 -181
  32. data/lib/quicsilver/client/client.rb +349 -0
  33. data/lib/quicsilver/client/connection_pool.rb +106 -0
  34. data/lib/quicsilver/client/request.rb +98 -0
  35. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  36. data/lib/quicsilver/protocol/adapter.rb +176 -0
  37. data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
  38. data/lib/quicsilver/protocol/frame_parser.rb +142 -0
  39. data/lib/quicsilver/protocol/frame_reader.rb +55 -0
  40. data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
  41. data/lib/quicsilver/protocol/priority.rb +56 -0
  42. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  43. data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
  44. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  45. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  46. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  47. data/lib/quicsilver/protocol/request_parser.rb +275 -0
  48. data/lib/quicsilver/protocol/response_encoder.rb +97 -0
  49. data/lib/quicsilver/protocol/response_parser.rb +141 -0
  50. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  51. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  52. data/lib/quicsilver/quicsilver.bundle +0 -0
  53. data/lib/quicsilver/server/listener_data.rb +14 -0
  54. data/lib/quicsilver/server/request_handler.rb +138 -0
  55. data/lib/quicsilver/server/request_registry.rb +50 -0
  56. data/lib/quicsilver/server/server.rb +610 -0
  57. data/lib/quicsilver/transport/configuration.rb +141 -0
  58. data/lib/quicsilver/transport/connection.rb +379 -0
  59. data/lib/quicsilver/transport/event_loop.rb +38 -0
  60. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  61. data/lib/quicsilver/transport/stream.rb +28 -0
  62. data/lib/quicsilver/transport/stream_event.rb +26 -0
  63. data/lib/quicsilver/version.rb +1 -1
  64. data/lib/quicsilver.rb +55 -14
  65. data/lib/rackup/handler/quicsilver.rb +1 -2
  66. data/quicsilver.gemspec +13 -3
  67. metadata +125 -21
  68. data/benchmarks/benchmark.rb +0 -68
  69. data/examples/setup_certs.sh +0 -57
  70. data/lib/quicsilver/client.rb +0 -261
  71. data/lib/quicsilver/connection.rb +0 -42
  72. data/lib/quicsilver/event_loop.rb +0 -38
  73. data/lib/quicsilver/http3/request_encoder.rb +0 -133
  74. data/lib/quicsilver/http3/request_parser.rb +0 -176
  75. data/lib/quicsilver/http3/response_encoder.rb +0 -186
  76. data/lib/quicsilver/http3/response_parser.rb +0 -160
  77. data/lib/quicsilver/listener_data.rb +0 -29
  78. data/lib/quicsilver/quic_stream.rb +0 -36
  79. data/lib/quicsilver/request_registry.rb +0 -48
  80. data/lib/quicsilver/server.rb +0 -355
  81. data/lib/quicsilver/server_configuration.rb +0 -78
@@ -1,261 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'http3/request_encoder'
4
- require_relative 'http3/response_parser'
5
- require_relative "event_loop"
6
- require "timeout"
7
-
8
- module Quicsilver
9
- class Client
10
- attr_reader :hostname, :port, :unsecure, :connection_timeout
11
-
12
- AlreadyConnectedError = Class.new(StandardError)
13
- NotConnectedError = Class.new(StandardError)
14
- StreamFailedToOpenError = Class.new(StandardError)
15
-
16
- FINISHED_EVENTS = %w[RECEIVE_FIN RECEIVE].freeze
17
-
18
- def initialize(hostname, port = 4433, options = {})
19
- @hostname = hostname
20
- @port = port
21
- @unsecure = options.fetch(:unsecure, true)
22
- @connection_timeout = options.fetch(:connection_timeout, 5000)
23
-
24
- @connection_data = nil
25
- @connected = false
26
- @connection_start_time = nil
27
-
28
- @response_buffers = {} # stream_id => accumulated data
29
- @pending_requests = {}
30
- @mutex = Mutex.new
31
- end
32
-
33
- def connect
34
- raise AlreadyConnectedError if @connected
35
-
36
- Quicsilver.open_connection
37
-
38
- config = Quicsilver.create_configuration(@unsecure)
39
- raise ConnectionError, "Failed to create configuration" if config.nil?
40
-
41
- # Create connection (returns [handle, context])
42
- # Pass self so C extension can route callbacks to this instance
43
- @connection_data = Quicsilver.create_connection(self)
44
- raise ConnectionError, "Failed to create connection" if @connection_data.nil?
45
-
46
- connection_handle, context_handle = @connection_data
47
-
48
- # Start the connection
49
- success = Quicsilver.start_connection(connection_handle, config, @hostname, @port)
50
- unless success
51
- Quicsilver.close_configuration(config)
52
- cleanup_failed_connection
53
- raise ConnectionError, "Failed to start connection"
54
- end
55
-
56
- result = Quicsilver.wait_for_connection(context_handle, @connection_timeout)
57
- handle_connection_result(result, config)
58
-
59
- @connected = true
60
- @connection_start_time = Time.now
61
-
62
- send_control_stream
63
- Quicsilver.close_configuration(config) # Clean up config since connection is established
64
-
65
- Quicsilver.event_loop.start
66
- self
67
- rescue => e
68
- cleanup_failed_connection
69
-
70
- if e.is_a?(ConnectionError) || e.is_a?(TimeoutError)
71
- raise e
72
- else
73
- raise ConnectionError, "Connection failed: #{e.message}"
74
- end
75
- end
76
-
77
- def disconnect
78
- return unless @connection_data
79
-
80
- @connected = false
81
-
82
- # Wake up pending requests
83
- @mutex.synchronize do
84
- @pending_requests.each_value { |q| q.push(nil) }
85
- @pending_requests.clear
86
- @response_buffers.clear
87
- end
88
-
89
- Quicsilver.close_connection_handle(@connection_data) if @connection_data
90
- @connection_data = nil
91
- end
92
-
93
- def get(path, **opts)
94
- request("GET", path, **opts)
95
- end
96
-
97
- def post(path, **opts)
98
- request("POST", path, **opts)
99
- end
100
-
101
- def patch(path, **opts)
102
- request("PATCH", path, **opts)
103
- end
104
-
105
- def delete(path, **opts)
106
- request("DELETE", path, **opts)
107
- end
108
-
109
- def head(path, **opts)
110
- request("HEAD", path, **opts)
111
- end
112
-
113
- def request(method, path, headers: {}, body: nil, timeout: 5000)
114
- raise NotConnectedError unless @connected
115
- response_queue = Queue.new
116
-
117
- request = HTTP3::RequestEncoder.new(
118
- method: method,
119
- path: path,
120
- scheme: "https",
121
- authority: authority,
122
- headers: headers,
123
- body: body
124
- )
125
-
126
- stream = open_stream
127
- raise StreamFailedToOpenError unless stream
128
-
129
- @mutex.synchronize do
130
- @pending_requests[stream] = response_queue
131
- end
132
-
133
- # Send data with FIN flag
134
- result = Quicsilver.send_stream(stream, request.encode, true)
135
-
136
- unless result
137
- @mutex.synchronize { @pending_requests.delete(stream) }
138
- raise Error, "Failed to send request"
139
- end
140
-
141
- response = response_queue.pop(timeout: timeout / 1000.0)
142
-
143
- raise ConnectionError, "Connection closed" if response.nil? && !@connected
144
- raise TimeoutError, "Request timeout after #{timeout}ms" if response.nil?
145
-
146
- response
147
- rescue Timeout::Error
148
- @mutex.synchronize { @pending_requests.delete(stream) } if stream
149
- end
150
-
151
- def connected?
152
- @connected && @connection_data && connection_alive?
153
- end
154
-
155
- def connection_info
156
- info = @connection_data ? Quicsilver.connection_status(@connection_data[1]) : {}
157
- info.merge(hostname: @hostname, port: @port, uptime: connection_uptime)
158
- end
159
-
160
- def connection_uptime
161
- return 0 unless @connection_start_time
162
- Time.now - @connection_start_time
163
- end
164
-
165
- def authority
166
- "#{@hostname}:#{@port}"
167
- end
168
-
169
- # Called directly by C extension via process_events
170
- # C extension routes to this instance based on client_obj stored in connection context
171
- # Clients should never call this method directly.
172
- def handle_stream_event(stream_id, event, data)
173
- return unless FINISHED_EVENTS.include?(event)
174
-
175
- @mutex.synchronize do
176
- case event
177
- when "RECEIVE"
178
- @response_buffers[stream_id] ||= StringIO.new
179
- @response_buffers[stream_id].write(data) # Buffer incoming response data
180
- when "RECEIVE_FIN"
181
- stream_handle = data[0, 8].unpack1('Q') if data.bytesize >= 8
182
- actual_data = data[8..-1] || ""
183
-
184
- # Get all buffered data
185
- buffer = @response_buffers.delete(stream_id)
186
- full_data = ( buffer&.string || "") + actual_data
187
-
188
- # TODO: needed for streaming later
189
- @stream_handles ||= {}
190
- @stream_handles[stream_id] = stream_handle if stream_handle
191
-
192
- response_parser = Quicsilver::HTTP3::ResponseParser.new(full_data)
193
- response_parser.parse
194
-
195
- # Store complete response with body as string
196
- response = {
197
- status: response_parser.status,
198
- headers: response_parser.headers,
199
- body: response_parser.body.read
200
- }
201
-
202
- queue = @pending_requests.delete(stream_handle)
203
- queue&.push(response) # Unblocks request
204
- end
205
- end
206
- rescue => e
207
- Quicsilver.logger.error("Error handling client stream: #{e.class} - #{e.message}")
208
- Quicsilver.logger.debug(e.backtrace.first(5).join("\n"))
209
- end
210
-
211
- private
212
-
213
- def cleanup_failed_connection
214
- Quicsilver.close_connection_handle(@connection_data) if @connection_data
215
- @connection_data = nil
216
- @connected = false
217
- end
218
-
219
- def open_stream
220
- Quicsilver.open_stream(@connection_data, false)
221
- end
222
-
223
- def open_unidirectional_stream
224
- Quicsilver.open_stream(@connection_data, true)
225
- end
226
-
227
- def send_control_stream
228
- # Open unidirectional stream
229
- stream = open_unidirectional_stream
230
-
231
- # Build and send control stream data
232
- control_data = Quicsilver::HTTP3.build_control_stream
233
- Quicsilver.send_stream(stream, control_data, false)
234
-
235
- @control_stream = stream
236
- end
237
-
238
- def handle_connection_result(result, config)
239
- if result.key?("error")
240
- error_status = result["status"]
241
- error_code = result["code"]
242
- Quicsilver.close_configuration(config)
243
- cleanup_failed_connection
244
- error_msg = "Connection failed with status: 0x#{error_status.to_s(16)}, code: #{error_code}"
245
- raise ConnectionError, error_msg
246
- elsif result.key?("timeout")
247
- Quicsilver.close_configuration(config)
248
- cleanup_failed_connection
249
- error_msg = "Connection timed out after #{@connection_timeout}ms"
250
- raise TimeoutError, error_msg
251
- end
252
- end
253
-
254
- def connection_alive?
255
- return false unless (info = Quicsilver.connection_status(@connection_data[1]))
256
- info["connected"] && !info["failed"]
257
- rescue
258
- false
259
- end
260
- end
261
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quicsilver
4
- class Connection
5
- attr_reader :handle, :data, :control_stream_id, :qpack_encoder_stream_id, :qpack_decoder_stream_id
6
- attr_reader :streams
7
- attr_accessor :server_control_stream # Handle for server's outbound control stream (used to send GOAWAY)
8
-
9
- def initialize(handle, data)
10
- @handle = handle
11
- @data = data
12
- @streams = {}
13
- @control_stream_id = nil
14
- @qpack_encoder_stream_id = nil
15
- @qpack_decoder_stream_id = nil
16
- end
17
-
18
- def set_qpack_encoder_stream(stream_id)
19
- @qpack_encoder_stream_id = stream_id
20
- end
21
-
22
- def set_qpack_decoder_stream(stream_id)
23
- @qpack_decoder_stream_id = stream_id
24
- end
25
-
26
- def set_control_stream(stream_id)
27
- @control_stream_id = stream_id
28
- end
29
-
30
- def add_stream(stream)
31
- @streams[stream.stream_id] = stream
32
- end
33
-
34
- def get_stream(stream_id)
35
- @streams[stream_id]
36
- end
37
-
38
- def remove_stream(stream_id)
39
- @streams.delete(stream_id)
40
- end
41
- end
42
- end
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quicsilver
4
- class EventLoop
5
- def initialize
6
- @running = false
7
- @thread = nil
8
- @mutex = Mutex.new
9
- end
10
-
11
- def start
12
- @mutex.synchronize do
13
- return if @running
14
-
15
- @running = true
16
- @thread = Thread.new do
17
- while @running
18
- processed = Quicsilver.process_events
19
- processed == 0 ? sleep(0.001) : Thread.pass
20
- end
21
- end
22
- end
23
- end
24
-
25
- def stop
26
- @running = false
27
- @thread&.join(2)
28
- end
29
-
30
- def join
31
- @thread&.join
32
- end
33
- end
34
-
35
- def self.event_loop
36
- @event_loop ||= EventLoop.new.tap(&:start)
37
- end
38
- end
@@ -1,133 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Quicsilver
4
- module HTTP3
5
- class RequestEncoder
6
- def initialize(method:, path:, scheme: 'https', authority: 'localhost:4433', headers: {}, body: nil)
7
- @method = method.upcase
8
- @path = path
9
- @scheme = scheme
10
- @authority = authority
11
- @headers = headers
12
- @body = body
13
- end
14
-
15
- def encode
16
- frames = []
17
-
18
- # Build HEADERS frame
19
- headers_payload = encode_headers
20
- frames << build_frame(HTTP3::FRAME_HEADERS, headers_payload)
21
-
22
- # Build DATA frame if body present
23
- if @body && !@body.empty?
24
- body_data = @body.is_a?(String) ? @body : @body.join
25
- frames << build_frame(HTTP3::FRAME_DATA, body_data)
26
- end
27
-
28
- frames.join.force_encoding(Encoding::BINARY)
29
- end
30
-
31
- private
32
-
33
- def build_frame(type, payload)
34
- frame_type = HTTP3.encode_varint(type)
35
- frame_length = HTTP3.encode_varint(payload.bytesize)
36
- frame_type + frame_length + payload
37
- end
38
-
39
- def encode_headers
40
- payload = "".b
41
-
42
- # QPACK prefix: Required Insert Count = 0, Delta Base = 0
43
- payload += "\x00\x00".b
44
-
45
- # Encode pseudo-headers using Indexed Field Line with Post-Base Index
46
- # Pattern: 0x50 (0101 0000) for :method, :scheme
47
- # Pattern: 0x40 | index for :authority, :path (literal name, literal value)
48
-
49
- # :method (use literal since GET/POST have specific indices but we want flexibility)
50
- payload += encode_literal_pseudo_header(':method', @method)
51
-
52
- # :scheme
53
- payload += encode_literal_pseudo_header(':scheme', @scheme)
54
-
55
- # :authority
56
- payload += encode_literal_pseudo_header(':authority', @authority)
57
-
58
- # :path
59
- payload += encode_literal_pseudo_header(':path', @path)
60
-
61
- # Encode regular headers
62
- @headers.each do |name, value|
63
- payload += encode_literal_header(name.to_s.downcase, value.to_s)
64
- end
65
-
66
- payload
67
- end
68
-
69
- # Literal field line with literal name for pseudo-headers
70
- # Pattern: 0x50 (indexed name from static table) + value
71
- def encode_literal_pseudo_header(name, value)
72
- result = "".b
73
-
74
- case name
75
- when ':method'
76
- # Check if exact match in static table
77
- index = case @method
78
- when 'GET' then HTTP3::QPACK_METHOD_GET
79
- when 'POST' then HTTP3::QPACK_METHOD_POST
80
- when 'PUT' then HTTP3::QPACK_METHOD_PUT
81
- when 'DELETE' then HTTP3::QPACK_METHOD_DELETE
82
- when 'CONNECT' then HTTP3::QPACK_METHOD_CONNECT
83
- when 'HEAD' then HTTP3::QPACK_METHOD_HEAD
84
- when 'OPTIONS' then HTTP3::QPACK_METHOD_OPTIONS
85
- else nil
86
- end
87
-
88
- if index
89
- # Exact match - use indexed field line (0x80 | index)
90
- result += [0x80 | index].pack('C')
91
- else
92
- # No exact match - use literal with name reference
93
- result += [0x40 | HTTP3::QPACK_METHOD_GET].pack('C') # Use any :method index for name
94
- result += HTTP3.encode_varint(value.bytesize)
95
- result += value.b
96
- end
97
-
98
- when ':scheme'
99
- # Check if exact match
100
- index = (@scheme == 'https' ? HTTP3::QPACK_SCHEME_HTTPS : HTTP3::QPACK_SCHEME_HTTP)
101
- # Exact match - use indexed field line
102
- result += [0x80 | index].pack('C')
103
-
104
- when ':authority', ':path'
105
- # Name in static table, but value is custom - use literal with name reference
106
- index = (name == ':authority' ? HTTP3::QPACK_AUTHORITY : HTTP3::QPACK_PATH)
107
- result += [0x40 | index].pack('C')
108
- result += HTTP3.encode_varint(value.bytesize)
109
- result += value.b
110
-
111
- else
112
- # Fallback to literal name
113
- return encode_literal_header(name, value)
114
- end
115
-
116
- result
117
- end
118
-
119
- # Literal field line with literal name
120
- # Pattern: 0x20 | name_length, name_bytes, value_length, value_bytes
121
- def encode_literal_header(name, value)
122
- result = "".b
123
- # 0x20 = literal with literal name (no indexing)
124
- name_len = name.bytesize
125
- result += [0x20 | (name_len & 0x1F)].pack('C')
126
- result += name.b
127
- result += HTTP3.encode_varint(value.bytesize)
128
- result += value.to_s.b
129
- result
130
- end
131
- end
132
- end
133
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'stringio'
4
-
5
- module Quicsilver
6
- module HTTP3
7
- class RequestParser
8
- attr_reader :frames, :headers, :body
9
-
10
- def initialize(data)
11
- @data = data
12
- @frames = []
13
- @headers = {}
14
- @body = StringIO.new
15
- @body.set_encoding(Encoding::ASCII_8BIT)
16
- end
17
-
18
- def parse
19
- parse!
20
- end
21
-
22
- def to_rack_env(stream_info = {})
23
- return nil if @headers.empty?
24
-
25
- # Extract path and query string
26
- path_full = @headers[':path'] || '/'
27
- path, query = path_full.split('?', 2)
28
-
29
- # Extract host and port
30
- authority = @headers[':authority'] || 'localhost:4433'
31
- host, port = authority.split(':', 2)
32
- port ||= '4433'
33
-
34
- env = {
35
- 'REQUEST_METHOD' => @headers[':method'] || 'GET',
36
- 'PATH_INFO' => path,
37
- 'QUERY_STRING' => query || '',
38
- 'SERVER_NAME' => host,
39
- 'SERVER_PORT' => port,
40
- 'SERVER_PROTOCOL' => 'HTTP/3',
41
- 'rack.version' => [1, 3],
42
- 'rack.url_scheme' => @headers[':scheme'] || 'https',
43
- 'rack.input' => @body,
44
- 'rack.errors' => $stderr,
45
- 'rack.multithread' => true,
46
- 'rack.multiprocess' => false,
47
- 'rack.run_once' => false,
48
- 'rack.hijack?' => false,
49
- 'SCRIPT_NAME' => '',
50
- 'CONTENT_LENGTH' => @body.size.to_s,
51
- }
52
-
53
- # Add HTTP_HOST from :authority pseudo-header
54
- if @headers[':authority']
55
- env['HTTP_HOST'] = @headers[':authority']
56
- end
57
-
58
- @headers.each do |name, value|
59
- next if name.start_with?(':')
60
- key = name.upcase.tr('-', '_')
61
- if key == 'CONTENT_TYPE'
62
- env['CONTENT_TYPE'] = value
63
- elsif key == 'CONTENT_LENGTH'
64
- env['CONTENT_LENGTH'] = value
65
- else
66
- env["HTTP_#{key}"] = value
67
- end
68
- end
69
-
70
- env
71
- end
72
-
73
- private
74
-
75
- def parse!
76
- buffer = @data.dup
77
- offset = 0
78
-
79
- while offset < buffer.bytesize
80
- break if buffer.bytesize - offset < 2
81
-
82
- type, type_len = HTTP3.decode_varint(buffer.bytes, offset)
83
- length, length_len = HTTP3.decode_varint(buffer.bytes, offset + type_len)
84
- header_len = type_len + length_len
85
-
86
- break if buffer.bytesize < offset + header_len + length
87
-
88
- payload = buffer[offset + header_len, length]
89
- @frames << { type: type, length: length, payload: payload }
90
-
91
- case type
92
- when 0x01 # HEADERS
93
- parse_headers(payload)
94
- when 0x00 # DATA
95
- @body.write(payload)
96
- end
97
-
98
- offset += header_len + length
99
- end
100
-
101
- @body.rewind
102
- end
103
-
104
- def parse_headers(payload)
105
- # Skip QPACK required insert count (1 byte) + delta base (1 byte)
106
- offset = 2
107
- return if payload.bytesize < offset
108
-
109
- while offset < payload.bytesize
110
- byte = payload.bytes[offset]
111
-
112
- # Pattern 1: Indexed Field Line (1Txxxxxx)
113
- # Use both name AND value from static table
114
- if (byte & 0x80) == 0x80
115
- index = byte & 0x3F
116
- offset += 1
117
-
118
- field = decode_static_table_field(index)
119
- if field.is_a?(Hash)
120
- @headers.merge!(field)
121
- end
122
- # Pattern 3: Literal with Name Reference (01NTxxxx)
123
- # Use name from static table, but value is provided as literal
124
- elsif (byte & 0xC0) == 0x40
125
- index = byte & 0x3F
126
- offset += 1
127
-
128
- # Get the name from static table
129
- entry = HTTP3::STATIC_TABLE[index] if index < HTTP3::STATIC_TABLE.size
130
- name = entry ? entry[0] : nil
131
-
132
- if name
133
- # Read literal value that follows
134
- value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
135
- offset += len_bytes
136
- value = payload[offset, value_len]
137
- offset += value_len
138
- @headers[name] = value
139
- end
140
- # Pattern 5: Literal with literal name (001NHxxx)
141
- elsif (byte & 0xE0) == 0x20
142
- name_len = byte & 0x1F
143
- offset += 1
144
- name = payload[offset, name_len]
145
- offset += name_len
146
-
147
- value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
148
- offset += len_bytes
149
- value = payload[offset, value_len]
150
- offset += value_len
151
-
152
- @headers[name] = value
153
- else
154
- break # Unknown encoding
155
- end
156
- end
157
- end
158
-
159
- # QPACK static table decoder (RFC 9204 Appendix A)
160
- # Returns Hash for complete fields, String for name-only fields
161
- def decode_static_table_field(index)
162
- return nil if index >= HTTP3::STATIC_TABLE.size
163
-
164
- name, value = HTTP3::STATIC_TABLE[index]
165
-
166
- # If value is empty, return just the name (caller provides value)
167
- # Otherwise return complete field as hash
168
- if value.empty?
169
- name
170
- else
171
- {name => value}
172
- end
173
- end
174
- end
175
- end
176
- end