quicsilver 0.5.0-x86_64-linux

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 (107) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +49 -0
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +21 -0
  5. data/.gitmodules +3 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +175 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +21 -0
  11. data/README.md +261 -0
  12. data/Rakefile +130 -0
  13. data/benchmarks/baseline.rb +175 -0
  14. data/benchmarks/compare.rb +398 -0
  15. data/benchmarks/components.rb +191 -0
  16. data/benchmarks/helpers.rb +162 -0
  17. data/benchmarks/latency.rb +128 -0
  18. data/benchmarks/loss.rb +193 -0
  19. data/benchmarks/mobile.rb +125 -0
  20. data/bin/console +14 -0
  21. data/bin/setup +8 -0
  22. data/docs/connection_identity.md +141 -0
  23. data/examples/README.md +58 -0
  24. data/examples/benchmark.rb +191 -0
  25. data/examples/connection_pool_demo.rb +47 -0
  26. data/examples/example_helper.rb +69 -0
  27. data/examples/falcon_middleware.rb +44 -0
  28. data/examples/feature_demo.rb +125 -0
  29. data/examples/grpc_style.rb +97 -0
  30. data/examples/latency_aware_routing.rb +382 -0
  31. data/examples/lifecycle_drain.rb +264 -0
  32. data/examples/minimal_http3_server.rb +14 -0
  33. data/examples/priorities.rb +60 -0
  34. data/examples/protocol_http_server.rb +31 -0
  35. data/examples/rack_http3_server.rb +42 -0
  36. data/examples/rails_feature_test.rb +280 -0
  37. data/examples/server_stats.rb +117 -0
  38. data/examples/simple_client_test.rb +26 -0
  39. data/examples/streaming_sse.rb +33 -0
  40. data/examples/trailers.rb +69 -0
  41. data/examples/webtransport_smoke.rb +546 -0
  42. data/examples/worker_load_balancing.rb +425 -0
  43. data/ext/quicsilver/extconf.rb +124 -0
  44. data/ext/quicsilver/include/msquic.h +2226 -0
  45. data/ext/quicsilver/include/msquic_posix.h +696 -0
  46. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  47. data/ext/quicsilver/msquic_patch.rb +68 -0
  48. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  49. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  50. data/ext/quicsilver/quicsilver.c +2741 -0
  51. data/lib/quicsilver/3.4/quicsilver.so +0 -0
  52. data/lib/quicsilver/4.0/quicsilver.so +0 -0
  53. data/lib/quicsilver/client/client.rb +708 -0
  54. data/lib/quicsilver/client/connection_pool.rb +202 -0
  55. data/lib/quicsilver/client/request.rb +190 -0
  56. data/lib/quicsilver/errors.rb +39 -0
  57. data/lib/quicsilver/libmsquic.so.2 +0 -0
  58. data/lib/quicsilver/protocol/adapter.rb +196 -0
  59. data/lib/quicsilver/protocol/capsule.rb +38 -0
  60. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  61. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  62. data/lib/quicsilver/protocol/datagram.rb +40 -0
  63. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  64. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  65. data/lib/quicsilver/protocol/frames.rb +454 -0
  66. data/lib/quicsilver/protocol/priority.rb +65 -0
  67. data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
  68. data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
  69. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  70. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  71. data/lib/quicsilver/protocol/request_encoder.rb +50 -0
  72. data/lib/quicsilver/protocol/request_parser.rb +289 -0
  73. data/lib/quicsilver/protocol/response_encoder.rb +107 -0
  74. data/lib/quicsilver/protocol/response_parser.rb +147 -0
  75. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  76. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  77. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  78. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  79. data/lib/quicsilver/rack/context.rb +33 -0
  80. data/lib/quicsilver/response.rb +40 -0
  81. data/lib/quicsilver/server/listener_data.rb +14 -0
  82. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  83. data/lib/quicsilver/server/receive_queue.rb +106 -0
  84. data/lib/quicsilver/server/request_handler.rb +149 -0
  85. data/lib/quicsilver/server/request_registry.rb +63 -0
  86. data/lib/quicsilver/server/scheduler.rb +50 -0
  87. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  88. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  89. data/lib/quicsilver/server/server.rb +1080 -0
  90. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  91. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  92. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  93. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  94. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  95. data/lib/quicsilver/transport/configuration.rb +234 -0
  96. data/lib/quicsilver/transport/connection.rb +594 -0
  97. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  98. data/lib/quicsilver/transport/event_loop.rb +74 -0
  99. data/lib/quicsilver/transport/inbound_stream.rb +53 -0
  100. data/lib/quicsilver/transport/stream.rb +43 -0
  101. data/lib/quicsilver/transport/stream_event.rb +49 -0
  102. data/lib/quicsilver/transport/stream_id.rb +26 -0
  103. data/lib/quicsilver/version.rb +3 -0
  104. data/lib/quicsilver.rb +96 -0
  105. data/lib/rackup/handler/quicsilver.rb +96 -0
  106. data/quicsilver.gemspec +66 -0
  107. metadata +362 -0
@@ -0,0 +1,217 @@
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
+ end
30
+
31
+ # Caches are keyed by header content, never by identity or object_id:
32
+ # callers may reuse and mutate the same array, and an identity hit
33
+ # would return the previous encoding.
34
+ #
35
+ # Caching a whole field block is only sound because encoding is
36
+ # static-table-only: Required Insert Count is always zero, so the bytes
37
+ # never depend on connection state. Implementing the dynamic table
38
+ # would invalidate @block_cache, since indices are relative to a Base.
39
+ def encode(headers)
40
+ if headers.is_a?(Array) && headers.size <= 16
41
+ # Content-based caching for small header sets
42
+ block_key = headers.map { |n, v| "#{n}\0#{v}" }.join("\x01")
43
+ cached_block = @block_cache[block_key]
44
+ return cached_block if cached_block
45
+
46
+ result_frozen = encode_fields(headers).freeze
47
+ if @block_cache.size < BLOCK_CACHE_MAX
48
+ @block_cache[block_key.freeze] = result_frozen
49
+ end
50
+ return result_frozen
51
+ end
52
+
53
+ encode_fields(headers)
54
+ end
55
+
56
+ BLOCK_CACHE_MAX = 128
57
+
58
+ private def encode_fields(headers)
59
+ out = encode_prefix
60
+ headers.each do |name, value|
61
+ name = name.to_s
62
+ value = value.to_s
63
+ # Downcase only if needed (most HTTP/3 headers are already lowercase)
64
+ name = name.downcase if name.match?(/[A-Z]/)
65
+
66
+ cache_key = "#{name}\0#{value}"
67
+
68
+ # Check field cache
69
+ cached = @field_cache[cache_key]
70
+ if cached
71
+ out << cached
72
+ next
73
+ end
74
+
75
+ field_start = out.bytesize
76
+
77
+ full_idx = STATIC_LOOKUP_FULL[cache_key]
78
+
79
+ if full_idx
80
+ # Indexed Field Line
81
+ out << encode_prefixed_int(full_idx, 6, 0xC0)
82
+ else
83
+ name_idx = STATIC_LOOKUP_NAME[name]
84
+ if name_idx
85
+ # Literal with Name Reference
86
+ out << encode_prefixed_int(name_idx, 4, 0x50)
87
+ encode_str_into(out, value)
88
+ else
89
+ # Literal with Literal Name
90
+ encode_literal_into(out, name, value)
91
+ end
92
+ end
93
+
94
+ # Cache the encoded field bytes
95
+ if @field_cache.size < FIELD_CACHE_MAX
96
+ @field_cache[cache_key.freeze] = out.byteslice(field_start..).freeze
97
+ end
98
+ end
99
+ out
100
+ end
101
+
102
+ def lookup(name, value)
103
+ name = name.to_s.downcase
104
+ value = value.to_s
105
+ full_idx = STATIC_LOOKUP_FULL["#{name}\0#{value}"]
106
+ return [full_idx, true] if full_idx
107
+ name_idx = STATIC_LOOKUP_NAME[name]
108
+ name_idx ? [name_idx, false] : nil
109
+ end
110
+
111
+ def encode_prefix
112
+ PREFIX.dup
113
+ end
114
+
115
+ # Public API for encoding a single string value (used by tests/external code)
116
+ def encode_str(value)
117
+ out = "".b
118
+ encode_str_into(out, value.to_s)
119
+ out
120
+ end
121
+
122
+ private
123
+
124
+ # Pattern 1: Indexed Field Line (1xxxxxxx) — kept for test compatibility
125
+ def encode_indexed(index)
126
+ encode_prefixed_int(index, 6, 0xC0)
127
+ end
128
+
129
+ def encode_literal_into(out, name, value)
130
+ name_b = name.b
131
+ if @huffman
132
+ huffman_name = Huffman.encode(name_b)
133
+ if huffman_name.bytesize < name_b.bytesize
134
+ out << encode_prefixed_int(huffman_name.bytesize, 3, 0x28)
135
+ out << huffman_name
136
+ encode_str_into(out, value)
137
+ return
138
+ end
139
+ end
140
+ out << encode_prefixed_int(name_b.bytesize, 3, 0x20)
141
+ out << name_b
142
+ encode_str_into(out, value)
143
+ end
144
+
145
+ def encode_str_into(out, value)
146
+ value_b = value.b
147
+ if @huffman
148
+ huffman = Huffman.encode(value_b)
149
+ if huffman.bytesize < value_b.bytesize
150
+ out << encode_prefixed_int(huffman.bytesize, 7, 0x80)
151
+ out << huffman
152
+ return
153
+ end
154
+ end
155
+ out << encode_prefixed_int(value_b.bytesize, 7, 0x00)
156
+ out << value_b
157
+ end
158
+
159
+ # Pre-computed prefix integer tables for hot encode paths.
160
+ # Key: [prefix_bits, pattern] => Array of frozen encoded strings indexed by value.
161
+ # Covers all values up to max_prefix (single-byte) and a range beyond.
162
+ PREFIXED_INT_CACHE = {}
163
+ [
164
+ [6, 0xC0], # Indexed Field Line
165
+ [4, 0x50], # Literal with Name Reference (static)
166
+ [7, 0x80], # Huffman string length
167
+ [7, 0x00], # Raw string length
168
+ [3, 0x28], # Huffman literal name length
169
+ [3, 0x20], # Raw literal name length
170
+ ].each do |prefix_bits, pattern|
171
+ max_prefix = (1 << prefix_bits) - 1
172
+ # Pre-compute single-byte range + a bit beyond for multi-byte
173
+ limit = [max_prefix + 64, 256].min
174
+ table = Array.new(limit) do |value|
175
+ if value < max_prefix
176
+ (pattern | value).chr(Encoding::BINARY).freeze
177
+ else
178
+ buf = (pattern | max_prefix).chr(Encoding::BINARY)
179
+ v = value - max_prefix
180
+ while v >= 128
181
+ buf << ((v & 0x7F) | 0x80).chr(Encoding::BINARY)
182
+ v >>= 7
183
+ end
184
+ buf << v.chr(Encoding::BINARY)
185
+ buf.freeze
186
+ end
187
+ end
188
+ PREFIXED_INT_CACHE[[prefix_bits, pattern]] = table.freeze
189
+ end
190
+ PREFIXED_INT_CACHE.freeze
191
+
192
+ # RFC 7541 prefix integer encoding
193
+ def encode_prefixed_int(value, prefix_bits, pattern)
194
+ table = PREFIXED_INT_CACHE[[prefix_bits, pattern]]
195
+ if table && value < table.size
196
+ return table[value]
197
+ end
198
+
199
+ max_prefix = (1 << prefix_bits) - 1
200
+
201
+ if value < max_prefix
202
+ (pattern | value).chr(Encoding::BINARY)
203
+ else
204
+ buf = (pattern | max_prefix).chr(Encoding::BINARY)
205
+ value -= max_prefix
206
+ while value >= 128
207
+ buf << ((value & 0x7F) | 0x80).chr(Encoding::BINARY)
208
+ value >>= 7
209
+ end
210
+ buf << value.chr(Encoding::BINARY)
211
+ buf
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,140 @@
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
+
55
+ # Decode Required Insert Count (RFC 9204 §4.5.1) — 8-bit prefix integer
56
+ required_insert_count, ric_bytes = decode_prefix_integer_str(payload, 0, 8, 0x00)
57
+ offset = ric_bytes
58
+
59
+ # Decode Delta Base (RFC 9204 §4.5.1) — 7-bit prefix integer with sign bit
60
+ delta_base, db_bytes = decode_prefix_integer_str(payload, offset, 7, 0x80)
61
+ offset += db_bytes
62
+
63
+ # Static-only mode: Required Insert Count and Delta Base must be 0
64
+ if required_insert_count != 0 || delta_base != 0
65
+ raise Protocol::FrameError.new(
66
+ "Dynamic QPACK table not supported (required_insert_count=#{required_insert_count}, delta_base=#{delta_base})",
67
+ error_code: Protocol::QPACK_DECOMPRESSION_FAILED
68
+ )
69
+ end
70
+
71
+ while offset < payload.bytesize
72
+ byte = payload.getbyte(offset)
73
+
74
+ # Indexed Field Line (1Txxxxxx) — name + value from static table
75
+ if (byte & 0x80) == 0x80
76
+ index, bytes_consumed = decode_prefix_integer_str(payload, offset, 6, 0xC0)
77
+ offset += bytes_consumed
78
+
79
+ if index < Protocol::STATIC_TABLE.size
80
+ name, value = Protocol::STATIC_TABLE[index]
81
+ headers << [name, value]
82
+ yield name, value
83
+ else
84
+ raise Protocol::FrameError.new(
85
+ "Invalid QPACK static table index #{index}",
86
+ error_code: Protocol::QPACK_DECOMPRESSION_FAILED
87
+ )
88
+ end
89
+
90
+ # Literal with Name Reference (01NTxxxx) — name from static table, literal value
91
+ elsif (byte & 0xC0) == 0x40
92
+ index, bytes_consumed = decode_prefix_integer_str(payload, offset, 4, 0xF0)
93
+ offset += bytes_consumed
94
+
95
+ if index >= Protocol::STATIC_TABLE.size
96
+ raise Protocol::FrameError.new(
97
+ "Invalid QPACK static table index #{index}",
98
+ error_code: Protocol::QPACK_DECOMPRESSION_FAILED
99
+ )
100
+ end
101
+
102
+ name = Protocol::STATIC_TABLE[index][0]
103
+ value, consumed = decode_qpack_string_from_str(payload, offset)
104
+ offset += consumed
105
+ headers << [name, value]
106
+ yield name, value
107
+
108
+ # Literal with Literal Name (001NHxxx) — both name and value are literals
109
+ elsif (byte & 0xE0) == 0x20
110
+ huffman_name = (byte & 0x08) != 0
111
+ name_len, name_len_bytes = decode_prefix_integer_str(payload, offset, 3, 0x28)
112
+ offset += name_len_bytes
113
+ raw_name = payload.byteslice(offset, name_len)
114
+ name = if huffman_name
115
+ Huffman.decode(raw_name) || raw_name
116
+ else
117
+ raw_name
118
+ end
119
+ offset += name_len
120
+
121
+ value, consumed = decode_qpack_string_from_str(payload, offset)
122
+ offset += consumed
123
+
124
+ headers << [name, value]
125
+ yield name, value
126
+ else
127
+ break
128
+ end
129
+ end
130
+
131
+ # Cache the result
132
+ if payload.bytesize <= 256 && @decode_cache.size < DECODE_CACHE_MAX
133
+ key = payload.frozen? ? payload : payload.dup.freeze
134
+ @decode_cache[key] = headers.freeze
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end