quicsilver 0.1.0 → 0.2.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.
@@ -9,65 +9,178 @@ module Quicsilver
9
9
  @body = body
10
10
  end
11
11
 
12
+ # Buffered encode - returns all frames at once (legacy)
12
13
  def encode
13
- frames = ""
14
+ frames = "".b
15
+ frames << encode_headers_frame
16
+ @body.each do |chunk|
17
+ frames << encode_data_frame(chunk) unless chunk.empty?
18
+ end
19
+ @body.close if @body.respond_to?(:close)
20
+ frames
21
+ end
14
22
 
15
- # HEADERS frame
16
- frames += encode_headers_frame
23
+ # Streaming encode - yields frames as they're ready
24
+ def stream_encode
25
+ yield encode_headers_frame, false
17
26
 
18
- # DATA frame(s)
27
+ last_chunk = nil
19
28
  @body.each do |chunk|
20
- frames += encode_data_frame(chunk) unless chunk.empty?
29
+ yield encode_data_frame(last_chunk), false if last_chunk && !last_chunk.empty?
30
+ last_chunk = chunk
21
31
  end
22
32
 
23
- @body.close if @body.respond_to?(:close)
33
+ # Send final chunk with FIN=true
34
+ if last_chunk && !last_chunk.empty?
35
+ yield encode_data_frame(last_chunk), true
36
+ else
37
+ yield "".b, true # Empty frame to signal FIN
38
+ end
24
39
 
25
- frames
40
+ @body.close if @body.respond_to?(:close)
26
41
  end
27
42
 
28
43
  private
29
44
 
30
45
  def encode_headers_frame
31
46
  payload = encode_qpack_response
32
-
33
- frame_type = HTTP3.encode_varint(0x01) # HEADERS
47
+ frame_type = HTTP3.encode_varint(HTTP3::FRAME_HEADERS)
34
48
  frame_length = HTTP3.encode_varint(payload.bytesize)
35
-
36
49
  frame_type + frame_length + payload
37
50
  end
38
51
 
39
52
  def encode_data_frame(data)
40
- frame_type = HTTP3.encode_varint(0x00) # DATA
41
- frame_length = HTTP3.encode_varint(data.bytesize)
42
-
43
- frame_type + frame_length + data
53
+ frame_type = HTTP3.encode_varint(HTTP3::FRAME_DATA)
54
+ data_bytes = data.to_s.b
55
+ frame_length = HTTP3.encode_varint(data_bytes.bytesize)
56
+ frame_type + frame_length + data_bytes
44
57
  end
45
58
 
46
59
  def encode_qpack_response
47
60
  # QPACK prefix: Required Insert Count = 0, Delta Base = 0
48
61
  encoded = [0x00, 0x00].pack('C*')
49
62
 
50
- # :status pseudo-header (literal indexed)
51
- status_str = @status.to_s
52
- encoded += [0x58, status_str.bytesize].pack('C*')
53
- encoded += status_str
63
+ # :status pseudo-header - use indexed if possible
64
+ encoded += encode_status(@status)
54
65
 
55
- # Regular headers (literal with literal name)
66
+ # Regular headers
56
67
  @headers.each do |name, value|
57
68
  next if name.start_with?('rack.') # Skip Rack internals
58
69
 
59
- name = name.downcase
70
+ name = name.to_s.downcase
60
71
  value = value.to_s
61
72
 
62
- # Literal field line with literal name (0x2X prefix)
63
- encoded += [0x20 | name.bytesize].pack('C')
64
- encoded += name
65
- encoded += HTTP3.encode_varint(value.bytesize)
66
- encoded += value
73
+ # Try to use indexed encoding for common headers
74
+ index = find_static_index(name, value)
75
+ if index
76
+ # Pattern 1: Indexed Field Line (1Txxxxxx where T=1 for static)
77
+ encoded += encode_indexed_field(index)
78
+ else
79
+ # Check if just the name exists in static table
80
+ name_index = find_static_name_index(name)
81
+ if name_index
82
+ # Pattern 3: Literal with name reference (01NTxxxx)
83
+ encoded += encode_literal_with_name_ref(name_index, value)
84
+ else
85
+ # Pattern 5: Literal with literal name (001NHxxx)
86
+ encoded += encode_literal_with_literal_name(name, value)
87
+ end
88
+ end
67
89
  end
68
90
 
69
91
  encoded
70
92
  end
93
+
94
+ private
95
+
96
+ def encode_status(status)
97
+ status_str = status.to_s
98
+ # Map common status codes to static table indices
99
+ index = case status_str
100
+ when '200' then HTTP3::QPACK_STATUS_200 # 25
101
+ when '404' then HTTP3::QPACK_STATUS_404 # 27
102
+ when '500' then HTTP3::QPACK_STATUS_500 # 71
103
+ when '400' then HTTP3::QPACK_STATUS_400 # 67
104
+ when '100' then 63
105
+ when '204' then 64
106
+ when '304' then 26
107
+ when '403' then 68
108
+ else nil
109
+ end
110
+
111
+ if index
112
+ # Indexed field line: 11 (pattern) + T (1=static) + index (prefix integer with N=6)
113
+ encode_indexed_field_with_prefix(index)
114
+ else
115
+ # Literal with name reference - use index 25 (:status 200) for name
116
+ name_ref = [0x40 | 25].pack('C')
117
+ status_bytes = status_str.b
118
+ length = [status_bytes.bytesize].pack('C')
119
+ name_ref + length + status_bytes
120
+ end
121
+ end
122
+
123
+ # Encode indexed field line using prefix integer encoding (RFC 7541)
124
+ # Pattern: 11 + T(1 bit) + index as prefix integer with N=6
125
+ def encode_indexed_field_with_prefix(index, prefix_bits: 6, pattern: 0xC0)
126
+ max_prefix = (1 << prefix_bits) - 1 # 2^6 - 1 = 63
127
+
128
+ if index < max_prefix
129
+ # Fits in prefix bits
130
+ [pattern | index].pack('C')
131
+ else
132
+ # Needs continuation bytes
133
+ result = [pattern | max_prefix].pack('C') # First byte: all prefix bits set to 1
134
+ remaining = index - max_prefix
135
+
136
+ # Encode remaining value using 7-bit continuation bytes
137
+ while remaining >= 128
138
+ result += [(remaining & 0x7F) | 0x80].pack('C') # MSB=1 means more bytes
139
+ remaining >>= 7
140
+ end
141
+ result += [remaining].pack('C') # Last byte: MSB=0
142
+
143
+ result
144
+ end
145
+ end
146
+
147
+ def find_static_index(name, value)
148
+ HTTP3::STATIC_TABLE.each_with_index do |(tbl_name, tbl_value), idx|
149
+ return idx if tbl_name == name && tbl_value == value
150
+ end
151
+ nil
152
+ end
153
+
154
+ def find_static_name_index(name)
155
+ HTTP3::STATIC_TABLE.each_with_index do |(tbl_name, _), idx|
156
+ return idx if tbl_name == name
157
+ end
158
+ nil
159
+ end
160
+
161
+ def encode_indexed_field(index)
162
+ # Pattern 1: Indexed Field Line with prefix integer encoding
163
+ encode_indexed_field_with_prefix(index)
164
+ end
165
+
166
+ def encode_literal_with_name_ref(name_index, value)
167
+ # Pattern 3: Literal Field Line with Name Reference
168
+ # 01 N T + 4-bit index (N=0, T=1 for static)
169
+ prefix = 0x40 | name_index
170
+ prefix_byte = [prefix].pack('C')
171
+ value_bytes = value.to_s.b
172
+ value_length = HTTP3.encode_varint(value_bytes.bytesize)
173
+ prefix_byte + value_length + value_bytes
174
+ end
175
+
176
+ def encode_literal_with_literal_name(name, value)
177
+ # Pattern 5: Literal Field Line with Literal Name
178
+ # 001 N H + 3-bit name length (N=0, H=0 for no Huffman)
179
+ prefix = 0x20 | name.bytesize
180
+ name_bytes = name.to_s.b
181
+ value_bytes = value.to_s.b
182
+ [prefix].pack('C') + name_bytes + HTTP3.encode_varint(value_bytes.bytesize) + value_bytes
183
+ end
71
184
  end
72
185
  end
73
186
  end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Quicsilver
6
+ module HTTP3
7
+ class ResponseParser
8
+ attr_reader :frames, :headers, :status
9
+
10
+ def initialize(data)
11
+ @data = data
12
+ @frames = []
13
+ @headers = {}
14
+ @body_io = StringIO.new
15
+ @status = nil
16
+ end
17
+
18
+ def body
19
+ @body_io.rewind
20
+ @body_io
21
+ end
22
+
23
+ def parse
24
+ parse!
25
+ end
26
+
27
+ private
28
+
29
+ def parse!
30
+ buffer = @data.dup
31
+ offset = 0
32
+
33
+ while offset < buffer.bytesize
34
+ break if buffer.bytesize - offset < 2
35
+
36
+ type, type_len = HTTP3.decode_varint(buffer.bytes, offset)
37
+ length, length_len = HTTP3.decode_varint(buffer.bytes, offset + type_len)
38
+ header_len = type_len + length_len
39
+
40
+ break if buffer.bytesize < offset + header_len + length
41
+
42
+ payload = buffer[offset + header_len, length]
43
+ @frames << { type: type, length: length, payload: payload }
44
+
45
+ case type
46
+ when 0x01 # HEADERS
47
+ parse_headers(payload)
48
+ when 0x00 # DATA
49
+ @body_io.write(payload)
50
+ end
51
+
52
+ offset += header_len + length
53
+ end
54
+ end
55
+
56
+ def parse_headers(payload)
57
+ # Skip QPACK required insert count (1 byte) + delta base (1 byte)
58
+ offset = 2
59
+ return if payload.bytesize < offset
60
+
61
+ while offset < payload.bytesize
62
+ byte = payload.bytes[offset]
63
+
64
+ # Pattern 1: Indexed Field Line (1Txxxxxx)
65
+ if (byte & 0x80) == 0x80
66
+ # Decode prefix integer with N=6 bits
67
+ index, bytes_consumed = decode_prefix_integer(payload.bytes, offset, 6, 0xC0)
68
+ offset += bytes_consumed
69
+
70
+ field = decode_static_table_field(index)
71
+ if field.is_a?(Hash)
72
+ field.each do |name, value|
73
+ if name == ":status"
74
+ @status = value.to_i
75
+ else
76
+ @headers[name] = value
77
+ end
78
+ end
79
+ end
80
+ # Pattern 3: Literal with Name Reference (01NTxxxx)
81
+ elsif (byte & 0xC0) == 0x40
82
+ index = byte & 0x3F
83
+ offset += 1
84
+
85
+ entry = HTTP3::STATIC_TABLE[index] if index < HTTP3::STATIC_TABLE.size
86
+ name = entry ? entry[0] : nil
87
+
88
+ if name
89
+ value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
90
+ offset += len_bytes
91
+ value = payload[offset, value_len]
92
+ offset += value_len
93
+
94
+ if name == ":status"
95
+ @status = value.to_i
96
+ else
97
+ @headers[name] = value
98
+ end
99
+ end
100
+ # Pattern 5: Literal with literal name (001NHxxx)
101
+ elsif (byte & 0xE0) == 0x20
102
+ name_len = byte & 0x1F
103
+ offset += 1
104
+ name = payload[offset, name_len]
105
+ offset += name_len
106
+
107
+ value_len, len_bytes = HTTP3.decode_varint(payload.bytes, offset)
108
+ offset += len_bytes
109
+ value = payload[offset, value_len]
110
+ offset += value_len
111
+
112
+ @headers[name] = value
113
+ else
114
+ break
115
+ end
116
+ end
117
+ end
118
+
119
+ def decode_static_table_field(index)
120
+ return nil if index >= HTTP3::STATIC_TABLE.size
121
+
122
+ name, value = HTTP3::STATIC_TABLE[index]
123
+
124
+ if value.empty?
125
+ name
126
+ else
127
+ {name => value}
128
+ end
129
+ end
130
+
131
+ # Decode prefix integer (RFC 7541)
132
+ # Returns [value, bytes_consumed]
133
+ def decode_prefix_integer(bytes, offset, prefix_bits, pattern_mask)
134
+ max_prefix = (1 << prefix_bits) - 1 # 2^N - 1
135
+
136
+ first_byte = bytes[offset]
137
+ value = first_byte & max_prefix
138
+ bytes_consumed = 1
139
+
140
+ # If all prefix bits are 1, value continues in next byte(s)
141
+ if value == max_prefix
142
+ multiplier = 1
143
+ loop do
144
+ return [value, bytes_consumed] if offset + bytes_consumed >= bytes.size
145
+
146
+ next_byte = bytes[offset + bytes_consumed]
147
+ bytes_consumed += 1
148
+
149
+ value += (next_byte & 0x7F) * multiplier
150
+ break if (next_byte & 0x80) == 0 # MSB=0 means last byte
151
+
152
+ multiplier *= 128
153
+ end
154
+ end
155
+
156
+ [value, bytes_consumed]
157
+ end
158
+ end
159
+ end
160
+ end
@@ -2,65 +2,219 @@
2
2
 
3
3
  module Quicsilver
4
4
  module HTTP3
5
- # Encode variable-length integer
6
- def self.encode_varint(value)
7
- case value
8
- when 0..63
9
- [value].pack('C')
10
- when 64..16383
11
- [0x40 | (value >> 8), value & 0xFF].pack('C*')
12
- when 16384..1073741823
13
- [0x80 | (value >> 24), (value >> 16) & 0xFF,
14
- (value >> 8) & 0xFF, value & 0xFF].pack('C*')
15
- else
16
- [0xC0 | (value >> 56), (value >> 48) & 0xFF,
17
- (value >> 40) & 0xFF, (value >> 32) & 0xFF,
18
- (value >> 24) & 0xFF, (value >> 16) & 0xFF,
19
- (value >> 8) & 0xFF, value & 0xFF].pack('C*')
5
+ # HTTP/3 Frame Types (RFC 9114)
6
+ FRAME_DATA = 0x00
7
+ FRAME_HEADERS = 0x01
8
+ FRAME_CANCEL_PUSH = 0x03
9
+ FRAME_SETTINGS = 0x04
10
+ FRAME_PUSH_PROMISE = 0x05
11
+ FRAME_GOAWAY = 0x07
12
+ FRAME_MAX_PUSH_ID = 0x0d
13
+
14
+ # QPACK Static Table Indices (RFC 9204 Appendix A)
15
+ STATIC_TABLE = [
16
+ [':authority', ''], # 0
17
+ [':path', '/'], # 1
18
+ ['age', '0'], # 2
19
+ ['content-disposition', ''], # 3
20
+ ['content-length', '0'], # 4
21
+ ['cookie', ''], # 5
22
+ ['date', ''], # 6
23
+ ['etag', ''], # 7
24
+ ['if-modified-since', ''], # 8
25
+ ['if-none-match', ''], # 9
26
+ ['last-modified', ''], # 10
27
+ ['link', ''], # 11
28
+ ['location', ''], # 12
29
+ ['referer', ''], # 13
30
+ ['set-cookie', ''], # 14
31
+ [':method', 'CONNECT'], # 15
32
+ [':method', 'DELETE'], # 16
33
+ [':method', 'GET'], # 17
34
+ [':method', 'HEAD'], # 18
35
+ [':method', 'OPTIONS'], # 19
36
+ [':method', 'POST'], # 20
37
+ [':method', 'PUT'], # 21
38
+ [':scheme', 'http'], # 22
39
+ [':scheme', 'https'], # 23
40
+ [':status', '103'], # 24
41
+ [':status', '200'], # 25
42
+ [':status', '304'], # 26
43
+ [':status', '404'], # 27
44
+ [':status', '503'], # 28
45
+ ['accept', '*/*'], # 29
46
+ ['accept', 'application/dns-message'], # 30
47
+ ['accept-encoding', 'gzip, deflate, br'], # 31
48
+ ['accept-ranges', 'bytes'], # 32
49
+ ['access-control-allow-headers', 'cache-control'], # 33
50
+ ['access-control-allow-headers', 'content-type'], # 34
51
+ ['access-control-allow-origin', '*'], # 35
52
+ ['cache-control', 'max-age=0'], # 36
53
+ ['cache-control', 'max-age=2592000'], # 37
54
+ ['cache-control', 'max-age=604800'], # 38
55
+ ['cache-control', 'no-cache'], # 39
56
+ ['cache-control', 'no-store'], # 40
57
+ ['cache-control', 'public, max-age=31536000'], # 41
58
+ ['content-encoding', 'br'], # 42
59
+ ['content-encoding', 'gzip'], # 43
60
+ ['content-type', 'application/dns-message'], # 44
61
+ ['content-type', 'application/javascript'], # 45
62
+ ['content-type', 'application/json'], # 46
63
+ ['content-type', 'application/x-www-form-urlencoded'], # 47
64
+ ['content-type', 'image/gif'], # 48
65
+ ['content-type', 'image/jpeg'], # 49
66
+ ['content-type', 'image/png'], # 50
67
+ ['content-type', 'text/css'], # 51
68
+ ['content-type', 'text/html; charset=utf-8'], # 52
69
+ ['content-type', 'text/plain'], # 53
70
+ ['content-type', 'text/plain;charset=utf-8'], # 54
71
+ ['range', 'bytes=0-'], # 55
72
+ ['strict-transport-security', 'max-age=31536000'], # 56
73
+ ['strict-transport-security', 'max-age=31536000; includesubdomains'], # 57
74
+ ['strict-transport-security', 'max-age=31536000; includesubdomains; preload'], # 58
75
+ ['vary', 'accept-encoding'], # 59
76
+ ['vary', 'origin'], # 60
77
+ ['x-content-type-options', 'nosniff'], # 61
78
+ ['x-xss-protection', '1; mode=block'], # 62
79
+ [':status', '100'], # 63
80
+ [':status', '204'], # 64
81
+ [':status', '206'], # 65
82
+ [':status', '302'], # 66
83
+ [':status', '400'], # 67
84
+ [':status', '403'], # 68
85
+ [':status', '421'], # 69
86
+ [':status', '425'], # 70
87
+ [':status', '500'], # 71
88
+ ['accept-language', ''], # 72
89
+ ['access-control-allow-credentials', 'FALSE'], # 73
90
+ ['access-control-allow-credentials', 'TRUE'], # 74
91
+ ['access-control-allow-headers', '*'], # 75
92
+ ['access-control-allow-methods', 'get'], # 76
93
+ ['access-control-allow-methods', 'get, post, options'], # 77
94
+ ['access-control-allow-methods', 'options'], # 78
95
+ ['access-control-expose-headers', 'content-length'], # 79
96
+ ['access-control-request-headers', 'content-type'], # 80
97
+ ['access-control-request-method', 'get'], # 81
98
+ ['access-control-request-method', 'post'], # 82
99
+ ['alt-svc', 'clear'], # 83
100
+ ['authorization', ''], # 84
101
+ ['content-security-policy', "script-src 'none'; object-src 'none'; base-uri 'none'"], # 85
102
+ ['early-data', '1'], # 86
103
+ ['expect-ct', ''], # 87
104
+ ['forwarded', ''], # 88
105
+ ['if-range', ''], # 89
106
+ ['origin', ''], # 90
107
+ ['purpose', 'prefetch'], # 91
108
+ ['server', ''], # 92
109
+ ['timing-allow-origin', '*'], # 93
110
+ ['upgrade-insecure-requests', '1'], # 94
111
+ ['user-agent', ''], # 95
112
+ ['x-forwarded-for', ''], # 96
113
+ ['x-frame-options', 'deny'], # 97
114
+ ['x-frame-options', 'sameorigin'] # 98
115
+ ].freeze
116
+
117
+ # Commonly used indices
118
+ QPACK_AUTHORITY = 0
119
+ QPACK_PATH = 1
120
+ QPACK_CONTENT_LENGTH = 4
121
+ QPACK_METHOD_CONNECT = 15
122
+ QPACK_METHOD_DELETE = 16
123
+ QPACK_METHOD_GET = 17
124
+ QPACK_METHOD_HEAD = 18
125
+ QPACK_METHOD_OPTIONS = 19
126
+ QPACK_METHOD_POST = 20
127
+ QPACK_METHOD_PUT = 21
128
+ QPACK_SCHEME_HTTP = 22
129
+ QPACK_SCHEME_HTTPS = 23
130
+ QPACK_STATUS_200 = 25
131
+ QPACK_STATUS_404 = 27
132
+ QPACK_STATUS_500 = 71
133
+ QPACK_STATUS_400 = 67
134
+ QPACK_CONTENT_TYPE_JSON = 46
135
+ QPACK_CONTENT_TYPE_PLAIN = 53
136
+
137
+ class << self
138
+ # Encode variable-length integer
139
+ def encode_varint(value)
140
+ case value
141
+ when 0..63
142
+ [value].pack('C')
143
+ when 64..16383
144
+ [0x40 | (value >> 8), value & 0xFF].pack('C*')
145
+ when 16384..1073741823
146
+ [0x80 | (value >> 24), (value >> 16) & 0xFF,
147
+ (value >> 8) & 0xFF, value & 0xFF].pack('C*')
148
+ else
149
+ [0xC0 | (value >> 56), (value >> 48) & 0xFF,
150
+ (value >> 40) & 0xFF, (value >> 32) & 0xFF,
151
+ (value >> 24) & 0xFF, (value >> 16) & 0xFF,
152
+ (value >> 8) & 0xFF, value & 0xFF].pack('C*')
153
+ end
20
154
  end
21
- end
22
155
 
23
- def self.build_settings_frame(settings = {})
24
- payload = ""
25
- settings.each do |id, value|
26
- payload += encode_varint(id)
27
- payload += encode_varint(value)
156
+ def build_settings_frame(settings = {})
157
+ payload = ""
158
+ settings.each do |id, value|
159
+ payload += encode_varint(id)
160
+ payload += encode_varint(value)
161
+ end
162
+
163
+ frame_type = encode_varint(FRAME_SETTINGS)
164
+ frame_length = encode_varint(payload.bytesize)
165
+
166
+ frame_type + frame_length + payload
28
167
  end
29
168
 
30
- frame_type = encode_varint(0x04) # SETTINGS
31
- frame_length = encode_varint(payload.bytesize)
169
+ # Build control stream data
170
+ def build_control_stream
171
+ stream_type = [0x00].pack('C') # Control stream type
172
+ settings = build_settings_frame({})
32
173
 
33
- frame_type + frame_length + payload
34
- end
174
+ stream_type + settings
175
+ end
35
176
 
36
- # Build control stream data
37
- def self.build_control_stream
38
- stream_type = [0x00].pack('C') # Control stream type
39
- settings = build_settings_frame({
40
- # 0x01 => 4096, # QPACK_MAX_TABLE_CAPACITY (optional)
41
- # 0x06 => 16384 # MAX_HEADER_LIST_SIZE (optional)
42
- })
177
+ # Build GOAWAY frame (RFC 9114 Section 7.2.6)
178
+ # stream_id: The last client-initiated bidirectional stream ID the server will process
179
+ def build_goaway_frame(stream_id)
180
+ frame_type = encode_varint(FRAME_GOAWAY)
181
+ payload = encode_varint(stream_id)
182
+ frame_length = encode_varint(payload.bytesize)
43
183
 
44
- stream_type + settings
45
- end
184
+ frame_type + frame_length + payload
185
+ end
186
+
187
+ # Maximum stream ID for initial GOAWAY (2^62 - 4, per RFC 9114)
188
+ MAX_STREAM_ID = (2**62) - 4
189
+
190
+ # Decode variable-length integer (RFC 9000)
191
+ # Returns [value, bytes_consumed]
192
+ def decode_varint(bytes, offset = 0)
193
+ return [0, 0] if offset >= bytes.size
194
+
195
+ first = bytes[offset]
196
+ return [0, 0] if first.nil?
197
+
198
+ prefix = (first & 0xC0) >> 6 # Extract 2 MSB
199
+ length = 1 << prefix # 1, 2, 4, or 8 bytes
200
+
201
+ # Check if we have enough bytes
202
+ return [0, 0] if offset + length > bytes.size
46
203
 
47
- # Decode variable-length integer (RFC 9000)
48
- # Returns [value, bytes_consumed]
49
- def self.decode_varint(bytes, offset = 0)
50
- first = bytes[offset]
51
- case (first & 0xC0) >> 6
52
- when 0
53
- [first & 0x3F, 1]
54
- when 1
55
- [(first & 0x3F) << 8 | bytes[offset + 1], 2]
56
- when 2
57
- [(first & 0x3F) << 24 | bytes[offset + 1] << 16 |
58
- bytes[offset + 2] << 8 | bytes[offset + 3], 4]
59
- else # when 3
60
- [(first & 0x3F) << 56 | bytes[offset + 1] << 48 |
61
- bytes[offset + 2] << 40 | bytes[offset + 3] << 32 |
62
- bytes[offset + 4] << 24 | bytes[offset + 5] << 16 |
63
- bytes[offset + 6] << 8 | bytes[offset + 7], 8]
204
+ case prefix
205
+ when 0
206
+ [first & 0x3F, 1]
207
+ when 1
208
+ [(first & 0x3F) << 8 | bytes[offset + 1], 2]
209
+ when 2
210
+ [(first & 0x3F) << 24 | bytes[offset + 1] << 16 |
211
+ bytes[offset + 2] << 8 | bytes[offset + 3], 4]
212
+ else # when 3
213
+ [(first & 0x3F) << 56 | bytes[offset + 1] << 48 |
214
+ bytes[offset + 2] << 40 | bytes[offset + 3] << 32 |
215
+ bytes[offset + 4] << 24 | bytes[offset + 5] << 16 |
216
+ bytes[offset + 6] << 8 | bytes[offset + 7], 8]
217
+ end
64
218
  end
65
219
  end
66
220
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quicsilver
4
+ class QuicStream
5
+ attr_reader :stream_id, :is_unidirectional, :buffer
6
+ attr_accessor :stream_handle
7
+
8
+ def initialize(stream_id, is_unidirectional: nil)
9
+ @stream_id = stream_id
10
+ @is_unidirectional = is_unidirectional.nil? ? !bidirectional? : is_unidirectional
11
+ @buffer = StringIO.new.tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
12
+ @stream_handle = nil
13
+ end
14
+
15
+ def bidirectional?
16
+ (stream_id & 0x02) == 0
17
+ end
18
+
19
+ def ready_to_send?
20
+ !stream_handle.nil?
21
+ end
22
+
23
+ def append_data(data)
24
+ @buffer.write(data)
25
+ end
26
+
27
+ def data
28
+ @buffer.string
29
+ end
30
+
31
+ def clear_buffer
32
+ @buffer.truncate(0)
33
+ @buffer.rewind
34
+ end
35
+ end
36
+ end