tigrcorn-protocols 0.3.16.dev5__py3-none-any.whl

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 (80) hide show
  1. tigrcorn_protocols/__init__.py +1 -0
  2. tigrcorn_protocols/_compression.py +219 -0
  3. tigrcorn_protocols/connect.py +107 -0
  4. tigrcorn_protocols/content_coding.py +179 -0
  5. tigrcorn_protocols/custom/__init__.py +3 -0
  6. tigrcorn_protocols/custom/adapters.py +18 -0
  7. tigrcorn_protocols/custom/registry.py +15 -0
  8. tigrcorn_protocols/flow/__init__.py +1 -0
  9. tigrcorn_protocols/flow/backpressure.py +17 -0
  10. tigrcorn_protocols/flow/buffers.py +29 -0
  11. tigrcorn_protocols/flow/credits.py +21 -0
  12. tigrcorn_protocols/flow/keepalive.py +85 -0
  13. tigrcorn_protocols/flow/timeouts.py +17 -0
  14. tigrcorn_protocols/flow/watermarks.py +16 -0
  15. tigrcorn_protocols/http1/__init__.py +16 -0
  16. tigrcorn_protocols/http1/keepalive.py +21 -0
  17. tigrcorn_protocols/http1/parser.py +481 -0
  18. tigrcorn_protocols/http1/serializer.py +198 -0
  19. tigrcorn_protocols/http1/state.py +9 -0
  20. tigrcorn_protocols/http2/__init__.py +16 -0
  21. tigrcorn_protocols/http2/codec.py +266 -0
  22. tigrcorn_protocols/http2/flow.py +35 -0
  23. tigrcorn_protocols/http2/handler.py +1303 -0
  24. tigrcorn_protocols/http2/hpack.py +393 -0
  25. tigrcorn_protocols/http2/state.py +226 -0
  26. tigrcorn_protocols/http2/streams.py +76 -0
  27. tigrcorn_protocols/http2/websocket.py +360 -0
  28. tigrcorn_protocols/http3/__init__.py +82 -0
  29. tigrcorn_protocols/http3/codec.py +148 -0
  30. tigrcorn_protocols/http3/handler/__init__.py +3 -0
  31. tigrcorn_protocols/http3/handler/core.py +1823 -0
  32. tigrcorn_protocols/http3/handler/webtransport.py +184 -0
  33. tigrcorn_protocols/http3/handler.py +3 -0
  34. tigrcorn_protocols/http3/qpack.py +843 -0
  35. tigrcorn_protocols/http3/state.py +129 -0
  36. tigrcorn_protocols/http3/streams.py +657 -0
  37. tigrcorn_protocols/http3/websocket.py +360 -0
  38. tigrcorn_protocols/lifespan/__init__.py +3 -0
  39. tigrcorn_protocols/lifespan/driver.py +83 -0
  40. tigrcorn_protocols/py.typed +1 -0
  41. tigrcorn_protocols/rawframed/__init__.py +5 -0
  42. tigrcorn_protocols/rawframed/codec.py +18 -0
  43. tigrcorn_protocols/rawframed/frames.py +28 -0
  44. tigrcorn_protocols/rawframed/handler.py +72 -0
  45. tigrcorn_protocols/rawframed/state.py +9 -0
  46. tigrcorn_protocols/registry.py +22 -0
  47. tigrcorn_protocols/scheduler/__init__.py +17 -0
  48. tigrcorn_protocols/scheduler/cancellation.py +40 -0
  49. tigrcorn_protocols/scheduler/dispatch.py +27 -0
  50. tigrcorn_protocols/scheduler/fairness.py +21 -0
  51. tigrcorn_protocols/scheduler/policy.py +12 -0
  52. tigrcorn_protocols/scheduler/priorities.py +8 -0
  53. tigrcorn_protocols/scheduler/quotas.py +19 -0
  54. tigrcorn_protocols/scheduler/runtime.py +156 -0
  55. tigrcorn_protocols/scheduler/tasks.py +31 -0
  56. tigrcorn_protocols/sessions/__init__.py +1 -0
  57. tigrcorn_protocols/sessions/base.py +16 -0
  58. tigrcorn_protocols/sessions/connection.py +12 -0
  59. tigrcorn_protocols/sessions/limits.py +12 -0
  60. tigrcorn_protocols/sessions/manager.py +31 -0
  61. tigrcorn_protocols/sessions/metadata.py +10 -0
  62. tigrcorn_protocols/sessions/quic.py +14 -0
  63. tigrcorn_protocols/streams/__init__.py +1 -0
  64. tigrcorn_protocols/streams/base.py +13 -0
  65. tigrcorn_protocols/streams/ids.py +5 -0
  66. tigrcorn_protocols/streams/multiplex.py +6 -0
  67. tigrcorn_protocols/streams/registry.py +22 -0
  68. tigrcorn_protocols/streams/singleplex.py +6 -0
  69. tigrcorn_protocols/websocket/__init__.py +1 -0
  70. tigrcorn_protocols/websocket/codec.py +31 -0
  71. tigrcorn_protocols/websocket/extensions.py +324 -0
  72. tigrcorn_protocols/websocket/frames.py +174 -0
  73. tigrcorn_protocols/websocket/handler.py +462 -0
  74. tigrcorn_protocols/websocket/handshake.py +66 -0
  75. tigrcorn_protocols/websocket/state.py +10 -0
  76. tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
  77. tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
  78. tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
  79. tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
  80. tigrcorn_protocols-0.3.16.dev5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,843 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from collections.abc import Callable, Iterable
5
+
6
+ from tigrcorn_core.errors import ProtocolError
7
+ from tigrcorn_protocols._compression import (
8
+ decode_prefixed_integer,
9
+ decode_prefixed_string,
10
+ encode_prefixed_integer,
11
+ encode_prefixed_string,
12
+ )
13
+
14
+ # RFC 9204 Appendix A static table (0-indexed).
15
+ _STATIC_TABLE: list[tuple[bytes, bytes]] = [
16
+ (b":authority", b""),
17
+ (b":path", b"/"),
18
+ (b"age", b"0"),
19
+ (b"content-disposition", b""),
20
+ (b"content-length", b"0"),
21
+ (b"cookie", b""),
22
+ (b"date", b""),
23
+ (b"etag", b""),
24
+ (b"if-modified-since", b""),
25
+ (b"if-none-match", b""),
26
+ (b"last-modified", b""),
27
+ (b"link", b""),
28
+ (b"location", b""),
29
+ (b"referer", b""),
30
+ (b"set-cookie", b""),
31
+ (b":method", b"CONNECT"),
32
+ (b":method", b"DELETE"),
33
+ (b":method", b"GET"),
34
+ (b":method", b"HEAD"),
35
+ (b":method", b"OPTIONS"),
36
+ (b":method", b"POST"),
37
+ (b":method", b"PUT"),
38
+ (b":scheme", b"http"),
39
+ (b":scheme", b"https"),
40
+ (b":status", b"103"),
41
+ (b":status", b"200"),
42
+ (b":status", b"304"),
43
+ (b":status", b"404"),
44
+ (b":status", b"503"),
45
+ (b"accept", b"*/*"),
46
+ (b"accept", b"application/dns-message"),
47
+ (b"accept-encoding", b"gzip, deflate, br"),
48
+ (b"accept-ranges", b"bytes"),
49
+ (b"access-control-allow-headers", b"cache-control"),
50
+ (b"access-control-allow-headers", b"content-type"),
51
+ (b"access-control-allow-origin", b"*"),
52
+ (b"cache-control", b"max-age=0"),
53
+ (b"cache-control", b"max-age=2592000"),
54
+ (b"cache-control", b"max-age=604800"),
55
+ (b"cache-control", b"no-cache"),
56
+ (b"cache-control", b"no-store"),
57
+ (b"cache-control", b"public, max-age=31536000"),
58
+ (b"content-encoding", b"br"),
59
+ (b"content-encoding", b"gzip"),
60
+ (b"content-type", b"application/dns-message"),
61
+ (b"content-type", b"application/javascript"),
62
+ (b"content-type", b"application/json"),
63
+ (b"content-type", b"application/x-www-form-urlencoded"),
64
+ (b"content-type", b"image/gif"),
65
+ (b"content-type", b"image/jpeg"),
66
+ (b"content-type", b"image/png"),
67
+ (b"content-type", b"text/css"),
68
+ (b"content-type", b"text/html; charset=utf-8"),
69
+ (b"content-type", b"text/plain"),
70
+ (b"content-type", b"text/plain;charset=utf-8"),
71
+ (b"range", b"bytes=0-"),
72
+ (b"strict-transport-security", b"max-age=31536000"),
73
+ (b"strict-transport-security", b"max-age=31536000; includesubdomains"),
74
+ (b"strict-transport-security", b"max-age=31536000; includesubdomains; preload"),
75
+ (b"vary", b"accept-encoding"),
76
+ (b"vary", b"origin"),
77
+ (b"x-content-type-options", b"nosniff"),
78
+ (b"x-xss-protection", b"1; mode=block"),
79
+ (b":status", b"100"),
80
+ (b":status", b"204"),
81
+ (b":status", b"206"),
82
+ (b":status", b"302"),
83
+ (b":status", b"400"),
84
+ (b":status", b"403"),
85
+ (b":status", b"421"),
86
+ (b":status", b"425"),
87
+ (b":status", b"500"),
88
+ (b"accept-language", b""),
89
+ (b"access-control-allow-credentials", b"FALSE"),
90
+ (b"access-control-allow-credentials", b"TRUE"),
91
+ (b"access-control-allow-headers", b"*"),
92
+ (b"access-control-allow-methods", b"get"),
93
+ (b"access-control-allow-methods", b"get, post, options"),
94
+ (b"access-control-allow-methods", b"options"),
95
+ (b"access-control-expose-headers", b"content-length"),
96
+ (b"access-control-request-headers", b"content-type"),
97
+ (b"access-control-request-method", b"get"),
98
+ (b"access-control-request-method", b"post"),
99
+ (b"alt-svc", b"clear"),
100
+ (b"authorization", b""),
101
+ (b"content-security-policy", b"script-src 'none'; object-src 'none'; base-uri 'none'"),
102
+ (b"early-data", b"1"),
103
+ (b"expect-ct", b""),
104
+ (b"forwarded", b""),
105
+ (b"if-range", b""),
106
+ (b"origin", b""),
107
+ (b"purpose", b"prefetch"),
108
+ (b"server", b""),
109
+ (b"timing-allow-origin", b"*"),
110
+ (b"upgrade-insecure-requests", b"1"),
111
+ (b"user-agent", b""),
112
+ (b"x-forwarded-for", b""),
113
+ (b"x-frame-options", b"deny"),
114
+ (b"x-frame-options", b"sameorigin"),
115
+ ]
116
+
117
+ STATIC_INDEX: dict[tuple[bytes, bytes], int] = {entry: idx for idx, entry in enumerate(_STATIC_TABLE)}
118
+ STATIC_NAME_INDEX: dict[bytes, int] = {}
119
+ for idx, (name, _value) in enumerate(_STATIC_TABLE):
120
+ if name not in STATIC_NAME_INDEX:
121
+ STATIC_NAME_INDEX[name] = idx
122
+
123
+ SENSITIVE_HEADERS = {
124
+ b"authorization",
125
+ b"cookie",
126
+ b"proxy-authorization",
127
+ b"set-cookie",
128
+ }
129
+
130
+
131
+ class QpackError(ProtocolError):
132
+ pass
133
+
134
+
135
+ class QpackBlocked(QpackError):
136
+ def __init__(self, required_insert_count: int) -> None:
137
+ super().__init__(f"QPACK field section is blocked on insert count {required_insert_count}")
138
+ self.required_insert_count = required_insert_count
139
+
140
+
141
+ class QpackDecompressionFailed(QpackError):
142
+ pass
143
+
144
+
145
+ class QpackEncoderStreamError(QpackError):
146
+ pass
147
+
148
+
149
+ class QpackDecoderStreamError(QpackError):
150
+ pass
151
+
152
+
153
+ @dataclass(slots=True)
154
+ class FieldLine:
155
+ name: bytes
156
+ value: bytes
157
+
158
+
159
+ @dataclass(slots=True)
160
+ class QpackFieldSection:
161
+ required_insert_count: int
162
+ base: int
163
+ headers: list[tuple[bytes, bytes]]
164
+ used_dynamic: bool = False
165
+
166
+
167
+ @dataclass(slots=True)
168
+ class QpackDynamicEntry:
169
+ absolute_index: int
170
+ name: bytes
171
+ value: bytes
172
+
173
+ @property
174
+ def size(self) -> int:
175
+ return len(self.name) + len(self.value) + 32
176
+
177
+
178
+ @dataclass(slots=True)
179
+ class _OutstandingSection:
180
+ required_insert_count: int
181
+ referenced_indexes: tuple[int, ...]
182
+
183
+
184
+ @dataclass(slots=True)
185
+ class _PlannedHeaderField:
186
+ kind: str
187
+ name: bytes
188
+ value: bytes
189
+ static_index: int | None = None
190
+ dynamic_absolute_index: int | None = None
191
+
192
+ def referenced_indexes(self) -> set[int]:
193
+ if self.dynamic_absolute_index is None:
194
+ return set()
195
+ return {self.dynamic_absolute_index}
196
+
197
+ def render(self, *, base: int, huffman: bool) -> bytes:
198
+ if self.kind == 'static_exact':
199
+ assert self.static_index is not None
200
+ return encode_qpack_integer(self.static_index, 6, 0xC0)
201
+ if self.kind == 'dynamic_exact':
202
+ assert self.dynamic_absolute_index is not None
203
+ relative_index = base - self.dynamic_absolute_index - 1
204
+ return encode_qpack_integer(relative_index, 6, 0x80)
205
+ if self.kind == 'static_name':
206
+ assert self.static_index is not None
207
+ return encode_qpack_integer(self.static_index, 4, 0x50) + encode_qpack_string(
208
+ self.value, 8, 0x00, huffman=huffman
209
+ )
210
+ if self.kind == 'dynamic_name':
211
+ assert self.dynamic_absolute_index is not None
212
+ relative_index = base - self.dynamic_absolute_index - 1
213
+ return encode_qpack_integer(relative_index, 4, 0x40) + encode_qpack_string(
214
+ self.value, 8, 0x00, huffman=huffman
215
+ )
216
+ if self.kind == 'literal':
217
+ return encode_qpack_string(self.name, 4, 0x20, huffman=huffman) + encode_qpack_string(
218
+ self.value, 8, 0x00, huffman=huffman
219
+ )
220
+ raise ProtocolError(f'unsupported QPACK header representation: {self.kind}')
221
+
222
+
223
+ @dataclass(slots=True)
224
+ class QpackDynamicTable:
225
+ maximum_capacity: int = 0
226
+ capacity: int = 0
227
+ entries: list[QpackDynamicEntry] = field(default_factory=list) # newest first
228
+ size: int = 0
229
+ insert_count: int = 0
230
+
231
+ def max_entries(self) -> int:
232
+ return self.maximum_capacity // 32 if self.maximum_capacity > 0 else 0
233
+
234
+ def set_capacity(self, capacity: int, *, evictable: Callable[[QpackDynamicEntry], bool] | None = None) -> None:
235
+ if capacity < 0 or capacity > self.maximum_capacity:
236
+ raise ProtocolError('QPACK dynamic table capacity out of range')
237
+ self.capacity = capacity
238
+ if not self._evict_to_limit(0, evictable=evictable):
239
+ raise ProtocolError('QPACK dynamic table capacity would evict a referenced entry')
240
+
241
+ def _evict_to_limit(
242
+ self,
243
+ incoming_size: int,
244
+ *,
245
+ evictable: Callable[[QpackDynamicEntry], bool] | None = None,
246
+ ) -> bool:
247
+ while self.size + incoming_size > self.capacity:
248
+ if not self.entries:
249
+ return False
250
+ evicted = self.entries[-1]
251
+ if evictable is not None and not evictable(evicted):
252
+ return False
253
+ self.entries.pop()
254
+ self.size -= evicted.size
255
+ return True
256
+
257
+ def can_insert(
258
+ self,
259
+ name: bytes,
260
+ value: bytes,
261
+ *,
262
+ evictable: Callable[[QpackDynamicEntry], bool] | None = None,
263
+ ) -> bool:
264
+ entry_size = len(name) + len(value) + 32
265
+ if entry_size > self.capacity:
266
+ return False
267
+ simulated_size = self.size
268
+ for entry in reversed(self.entries):
269
+ if simulated_size + entry_size <= self.capacity:
270
+ break
271
+ if evictable is not None and not evictable(entry):
272
+ return False
273
+ simulated_size -= entry.size
274
+ return simulated_size + entry_size <= self.capacity
275
+
276
+ def insert(
277
+ self,
278
+ name: bytes,
279
+ value: bytes,
280
+ *,
281
+ evictable: Callable[[QpackDynamicEntry], bool] | None = None,
282
+ ) -> QpackDynamicEntry:
283
+ entry = QpackDynamicEntry(absolute_index=self.insert_count, name=name, value=value)
284
+ if entry.size > self.capacity:
285
+ raise ProtocolError('QPACK dynamic entry exceeds table capacity')
286
+ if not self._evict_to_limit(entry.size, evictable=evictable):
287
+ raise ProtocolError('QPACK dynamic entry would evict a referenced entry')
288
+ self.entries.insert(0, entry)
289
+ self.size += entry.size
290
+ self.insert_count += 1
291
+ return entry
292
+
293
+ def duplicate_relative(
294
+ self,
295
+ relative_index: int,
296
+ *,
297
+ evictable: Callable[[QpackDynamicEntry], bool] | None = None,
298
+ ) -> QpackDynamicEntry:
299
+ entry = self.lookup_instruction_relative(relative_index)
300
+ return self.insert(entry.name, entry.value, evictable=evictable)
301
+
302
+ def lookup_static(self, index: int) -> tuple[bytes, bytes]:
303
+ if index < 0 or index >= len(_STATIC_TABLE):
304
+ raise ProtocolError(f'unsupported QPACK static index: {index}')
305
+ return _STATIC_TABLE[index]
306
+
307
+ def lookup_absolute_entry(self, absolute_index: int) -> QpackDynamicEntry:
308
+ for entry in self.entries:
309
+ if entry.absolute_index == absolute_index:
310
+ return entry
311
+ raise ProtocolError(f'unknown QPACK dynamic index: {absolute_index}')
312
+
313
+ def lookup_absolute(self, absolute_index: int) -> tuple[bytes, bytes]:
314
+ entry = self.lookup_absolute_entry(absolute_index)
315
+ return entry.name, entry.value
316
+
317
+ def absolute_index_from_relative(self, base: int, relative_index: int) -> int:
318
+ absolute_index = base - relative_index - 1
319
+ if absolute_index < 0:
320
+ raise ProtocolError('invalid QPACK relative index')
321
+ return absolute_index
322
+
323
+ def absolute_index_from_post_base(self, base: int, post_base_index: int) -> int:
324
+ absolute_index = base + post_base_index
325
+ if absolute_index < 0:
326
+ raise ProtocolError('invalid QPACK post-base index')
327
+ return absolute_index
328
+
329
+ def lookup_relative(self, base: int, relative_index: int) -> tuple[bytes, bytes]:
330
+ return self.lookup_absolute(self.absolute_index_from_relative(base, relative_index))
331
+
332
+ def lookup_post_base(self, base: int, post_base_index: int) -> tuple[bytes, bytes]:
333
+ return self.lookup_absolute(self.absolute_index_from_post_base(base, post_base_index))
334
+
335
+ def lookup_instruction_relative(self, relative_index: int) -> QpackDynamicEntry:
336
+ absolute_index = self.insert_count - relative_index - 1
337
+ if absolute_index < 0:
338
+ raise ProtocolError('invalid QPACK instruction relative index')
339
+ return self.lookup_absolute_entry(absolute_index)
340
+
341
+ def lookup_dynamic_exact(self, name: bytes, value: bytes, *, max_absolute_index: int | None = None) -> QpackDynamicEntry | None:
342
+ for entry in self.entries:
343
+ if max_absolute_index is not None and entry.absolute_index >= max_absolute_index:
344
+ continue
345
+ if entry.name == name and entry.value == value:
346
+ return entry
347
+ return None
348
+
349
+ def lookup_dynamic_name(self, name: bytes, *, max_absolute_index: int | None = None) -> QpackDynamicEntry | None:
350
+ for entry in self.entries:
351
+ if max_absolute_index is not None and entry.absolute_index >= max_absolute_index:
352
+ continue
353
+ if entry.name == name:
354
+ return entry
355
+ return None
356
+
357
+
358
+ # Wire helpers
359
+
360
+ def encode_qpack_integer(value: int, prefix_bits: int, prefix_mask: int = 0) -> bytes:
361
+ return encode_prefixed_integer(value, prefix_bits, prefix_mask)
362
+
363
+
364
+ def decode_qpack_integer(data: bytes, offset: int, prefix_bits: int) -> tuple[int, int]:
365
+ return decode_prefixed_integer(data, offset, prefix_bits)
366
+
367
+
368
+ def encode_qpack_string(data: bytes, prefix_bits: int = 8, prefix_mask: int = 0, *, huffman: bool = True) -> bytes:
369
+ return encode_prefixed_string(data, prefix_bits, prefix_mask, huffman=huffman)
370
+
371
+
372
+ def decode_qpack_string(data: bytes, offset: int, prefix_bits: int = 8) -> tuple[bytes, int]:
373
+ return decode_prefixed_string(data, offset, prefix_bits)
374
+
375
+
376
+ # Encoder stream instructions.
377
+ def encode_set_dynamic_table_capacity(capacity: int) -> bytes:
378
+ return encode_qpack_integer(capacity, 5, 0x20)
379
+
380
+
381
+ def encode_insert_with_name_reference(name_index: int, value: bytes, *, static: bool, huffman: bool = True) -> bytes:
382
+ return encode_qpack_integer(name_index, 6, 0xC0 if static else 0x80) + encode_qpack_string(
383
+ value, 8, 0x00, huffman=huffman
384
+ )
385
+
386
+
387
+ def encode_insert_with_literal_name(name: bytes, value: bytes, *, huffman: bool = True) -> bytes:
388
+ return encode_qpack_string(name, 6, 0x40, huffman=huffman) + encode_qpack_string(value, 8, 0x00, huffman=huffman)
389
+
390
+
391
+ def encode_duplicate(relative_index: int) -> bytes:
392
+ return encode_qpack_integer(relative_index, 5, 0x00)
393
+
394
+
395
+ # Decoder stream instructions.
396
+ def encode_section_ack(stream_id: int) -> bytes:
397
+ return encode_qpack_integer(stream_id, 7, 0x80)
398
+
399
+
400
+ def encode_stream_cancellation(stream_id: int) -> bytes:
401
+ return encode_qpack_integer(stream_id, 6, 0x40)
402
+
403
+
404
+ def encode_insert_count_increment(increment: int) -> bytes:
405
+ if increment <= 0:
406
+ raise ProtocolError('QPACK insert count increment must be positive')
407
+ return encode_qpack_integer(increment, 6, 0x00)
408
+
409
+
410
+ class QpackEncoder:
411
+ def __init__(
412
+ self,
413
+ *,
414
+ max_table_capacity: int = 0,
415
+ blocked_streams: int = 0,
416
+ use_huffman: bool = True,
417
+ sensitive_headers: set[bytes] | None = None,
418
+ ) -> None:
419
+ self.dynamic_table = QpackDynamicTable(maximum_capacity=max_table_capacity, capacity=0)
420
+ self.blocked_streams = blocked_streams
421
+ self.use_huffman = use_huffman
422
+ self.sensitive_headers = set(SENSITIVE_HEADERS if sensitive_headers is None else sensitive_headers)
423
+ self.known_received_count = 0
424
+ self._pending_encoder_bytes = bytearray()
425
+ self._announced_capacity = 0
426
+ self._outstanding_sections: dict[int, list[_OutstandingSection]] = {}
427
+ self._reference_counts: dict[int, int] = {}
428
+
429
+ def _evictable_entry(self, entry: QpackDynamicEntry) -> bool:
430
+ return entry.absolute_index < self.known_received_count and self._reference_counts.get(entry.absolute_index, 0) == 0
431
+
432
+ def _ensure_capacity_announced(self) -> None:
433
+ target = self.dynamic_table.maximum_capacity
434
+ if target > 0 and self._announced_capacity != target:
435
+ self.dynamic_table.set_capacity(target, evictable=self._evictable_entry)
436
+ self._pending_encoder_bytes.extend(encode_set_dynamic_table_capacity(target))
437
+ self._announced_capacity = target
438
+
439
+ def _should_index(self, name: bytes, value: bytes) -> bool:
440
+ if self.dynamic_table.maximum_capacity <= 0 or name in self.sensitive_headers:
441
+ return False
442
+ return self.dynamic_table.can_insert(name, value, evictable=self._evictable_entry)
443
+
444
+ def _queue_insert(self, name: bytes, value: bytes) -> QpackDynamicEntry:
445
+ static_name_index = STATIC_NAME_INDEX.get(name)
446
+ dynamic_name_entry = self.dynamic_table.lookup_dynamic_name(name)
447
+ if static_name_index is not None:
448
+ self._pending_encoder_bytes.extend(
449
+ encode_insert_with_name_reference(static_name_index, value, static=True, huffman=self.use_huffman)
450
+ )
451
+ elif dynamic_name_entry is not None:
452
+ relative_index = self.dynamic_table.insert_count - dynamic_name_entry.absolute_index - 1
453
+ self._pending_encoder_bytes.extend(
454
+ encode_insert_with_name_reference(relative_index, value, static=False, huffman=self.use_huffman)
455
+ )
456
+ else:
457
+ self._pending_encoder_bytes.extend(encode_insert_with_literal_name(name, value, huffman=self.use_huffman))
458
+ return self.dynamic_table.insert(name, value, evictable=self._evictable_entry)
459
+
460
+ def _encode_prefix(self, required_insert_count: int, base: int) -> bytes:
461
+ max_entries = self.dynamic_table.max_entries()
462
+ if required_insert_count == 0:
463
+ encoded_required = 0
464
+ else:
465
+ if max_entries <= 0:
466
+ raise ProtocolError('QPACK dynamic references require non-zero table capacity')
467
+ encoded_required = (required_insert_count % (2 * max_entries)) + 1
468
+ if base < required_insert_count:
469
+ sign = 1
470
+ delta = required_insert_count - base - 1
471
+ else:
472
+ sign = 0
473
+ delta = base - required_insert_count
474
+ return encode_qpack_integer(encoded_required, 8, 0x00) + encode_qpack_integer(delta, 7, 0x80 if sign else 0x00)
475
+
476
+ def _blocked_stream_ids(self) -> set[int]:
477
+ blocked: set[int] = set()
478
+ for stream_id, sections in self._outstanding_sections.items():
479
+ if any(section.required_insert_count > self.known_received_count for section in sections):
480
+ blocked.add(stream_id)
481
+ return blocked
482
+
483
+ def _can_risk_blocking(self, stream_id: int) -> bool:
484
+ if self.blocked_streams <= 0:
485
+ return False
486
+ blocked_stream_ids = self._blocked_stream_ids()
487
+ return stream_id in blocked_stream_ids or len(blocked_stream_ids) < self.blocked_streams
488
+
489
+ def _plan_header(self, name: bytes, value: bytes, *, reference_limit: int) -> _PlannedHeaderField:
490
+ static_exact = STATIC_INDEX.get((name, value))
491
+ if static_exact is not None:
492
+ return _PlannedHeaderField(kind='static_exact', name=name, value=value, static_index=static_exact)
493
+ dynamic_exact = self.dynamic_table.lookup_dynamic_exact(name, value, max_absolute_index=reference_limit)
494
+ if dynamic_exact is not None:
495
+ return _PlannedHeaderField(
496
+ kind='dynamic_exact',
497
+ name=name,
498
+ value=value,
499
+ dynamic_absolute_index=dynamic_exact.absolute_index,
500
+ )
501
+ static_name = STATIC_NAME_INDEX.get(name)
502
+ if static_name is not None:
503
+ return _PlannedHeaderField(kind='static_name', name=name, value=value, static_index=static_name)
504
+ dynamic_name = self.dynamic_table.lookup_dynamic_name(name, max_absolute_index=reference_limit)
505
+ if dynamic_name is not None:
506
+ return _PlannedHeaderField(
507
+ kind='dynamic_name',
508
+ name=name,
509
+ value=value,
510
+ dynamic_absolute_index=dynamic_name.absolute_index,
511
+ )
512
+ return _PlannedHeaderField(kind='literal', name=name, value=value)
513
+
514
+ def _track_outstanding_section(self, stream_id: int, *, required_insert_count: int, referenced_indexes: set[int]) -> None:
515
+ if required_insert_count <= 0:
516
+ return
517
+ ordered_indexes = tuple(sorted(referenced_indexes))
518
+ self._outstanding_sections.setdefault(stream_id, []).append(
519
+ _OutstandingSection(required_insert_count=required_insert_count, referenced_indexes=ordered_indexes)
520
+ )
521
+ for absolute_index in ordered_indexes:
522
+ self._reference_counts[absolute_index] = self._reference_counts.get(absolute_index, 0) + 1
523
+
524
+ def _release_section(self, section: _OutstandingSection) -> None:
525
+ for absolute_index in section.referenced_indexes:
526
+ remaining = self._reference_counts.get(absolute_index, 0) - 1
527
+ if remaining > 0:
528
+ self._reference_counts[absolute_index] = remaining
529
+ else:
530
+ self._reference_counts.pop(absolute_index, None)
531
+
532
+ def encode_field_section(self, headers: Iterable[tuple[bytes, bytes]], *, stream_id: int = 0) -> bytes:
533
+ header_list = [(bytes(name), bytes(value)) for name, value in headers]
534
+ allow_blocking = self._can_risk_blocking(stream_id)
535
+ if self.dynamic_table.maximum_capacity > 0:
536
+ self._ensure_capacity_announced()
537
+ if allow_blocking:
538
+ inserted: set[tuple[bytes, bytes]] = set()
539
+ for name, value in header_list:
540
+ if not self._should_index(name, value):
541
+ continue
542
+ if STATIC_INDEX.get((name, value)) is not None:
543
+ continue
544
+ if self.dynamic_table.lookup_dynamic_exact(name, value) is not None:
545
+ continue
546
+ candidate = (name, value)
547
+ if candidate in inserted:
548
+ continue
549
+ try:
550
+ self._queue_insert(name, value)
551
+ except ProtocolError:
552
+ continue
553
+ inserted.add(candidate)
554
+ reference_limit = self.dynamic_table.insert_count if allow_blocking else self.known_received_count
555
+ plans = [self._plan_header(name, value, reference_limit=reference_limit) for name, value in header_list]
556
+ referenced_indexes: set[int] = set()
557
+ for plan in plans:
558
+ referenced_indexes.update(plan.referenced_indexes())
559
+ required_insert_count = max((absolute_index + 1 for absolute_index in referenced_indexes), default=0)
560
+ base = required_insert_count
561
+ encoded = bytearray(self._encode_prefix(required_insert_count, base))
562
+ for plan in plans:
563
+ encoded.extend(plan.render(base=base, huffman=self.use_huffman))
564
+ self._track_outstanding_section(stream_id, required_insert_count=required_insert_count, referenced_indexes=referenced_indexes)
565
+ return bytes(encoded)
566
+
567
+ def receive_decoder_stream(self, data: bytes) -> None:
568
+ offset = 0
569
+ while offset < len(data):
570
+ first = data[offset]
571
+ if first & 0x80:
572
+ stream_id, offset = decode_qpack_integer(data, offset, 7)
573
+ outstanding = self._outstanding_sections.get(stream_id)
574
+ if not outstanding:
575
+ raise QpackDecoderStreamError('unexpected QPACK section acknowledgment')
576
+ section = outstanding.pop(0)
577
+ self._release_section(section)
578
+ self.known_received_count = max(self.known_received_count, section.required_insert_count)
579
+ if not outstanding:
580
+ self._outstanding_sections.pop(stream_id, None)
581
+ continue
582
+ if first & 0x40:
583
+ stream_id, offset = decode_qpack_integer(data, offset, 6)
584
+ cancelled = self._outstanding_sections.pop(stream_id, [])
585
+ for section in cancelled:
586
+ self._release_section(section)
587
+ continue
588
+ increment, offset = decode_qpack_integer(data, offset, 6)
589
+ if increment <= 0:
590
+ raise QpackDecoderStreamError('invalid QPACK insert count increment')
591
+ if self.known_received_count + increment > self.dynamic_table.insert_count:
592
+ raise QpackDecoderStreamError('QPACK insert count increment exceeds sent inserts')
593
+ self.known_received_count += increment
594
+
595
+ def take_encoder_stream_data(self) -> bytes:
596
+ payload = bytes(self._pending_encoder_bytes)
597
+ self._pending_encoder_bytes.clear()
598
+ return payload
599
+
600
+
601
+ class QpackDecoder:
602
+ def __init__(self, *, max_table_capacity: int = 0, blocked_streams: int = 0) -> None:
603
+ self.dynamic_table = QpackDynamicTable(maximum_capacity=max_table_capacity, capacity=0)
604
+ self.blocked_streams = blocked_streams
605
+ self.known_received_count = 0
606
+ self._pending_decoder_bytes = bytearray()
607
+ self._blocked_requirements: dict[int, list[int]] = {}
608
+
609
+ def _decode_required_insert_count(self, encoded_required: int) -> int:
610
+ max_entries = self.dynamic_table.max_entries()
611
+ if encoded_required == 0:
612
+ return 0
613
+ if max_entries <= 0:
614
+ raise QpackDecompressionFailed('QPACK dynamic references require non-zero table capacity')
615
+ full_range = 2 * max_entries
616
+ if encoded_required > full_range:
617
+ raise QpackDecompressionFailed('invalid QPACK encoded required insert count')
618
+ max_value = self.dynamic_table.insert_count + max_entries
619
+ max_wrapped = (max_value // full_range) * full_range
620
+ required = max_wrapped + encoded_required - 1
621
+ if required > max_value:
622
+ if required <= full_range:
623
+ raise QpackDecompressionFailed('invalid QPACK required insert count')
624
+ required -= full_range
625
+ if required == 0:
626
+ raise QpackDecompressionFailed('QPACK zero required insert count must be encoded as zero')
627
+ return required
628
+
629
+ def _mark_blocked(self, stream_id: int | None, required_insert_count: int) -> None:
630
+ if stream_id is None:
631
+ return
632
+ blocked = self._blocked_requirements.get(stream_id)
633
+ if blocked is None:
634
+ if len(self._blocked_requirements) >= self.blocked_streams:
635
+ raise QpackDecompressionFailed('QPACK blocked streams limit exceeded')
636
+ blocked = []
637
+ self._blocked_requirements[stream_id] = blocked
638
+ blocked.append(required_insert_count)
639
+
640
+ def _unmark_blocked(self, stream_id: int | None, required_insert_count: int) -> None:
641
+ if stream_id is None:
642
+ return
643
+ blocked = self._blocked_requirements.get(stream_id)
644
+ if not blocked:
645
+ return
646
+ try:
647
+ blocked.remove(required_insert_count)
648
+ except ValueError:
649
+ return
650
+ if not blocked:
651
+ self._blocked_requirements.pop(stream_id, None)
652
+
653
+ def _lookup_encoder_stream_name(self, *, static: bool, name_index: int) -> bytes:
654
+ try:
655
+ if static:
656
+ name, _value = self.dynamic_table.lookup_static(name_index)
657
+ return name
658
+ entry = self.dynamic_table.lookup_instruction_relative(name_index)
659
+ return entry.name
660
+ except ProtocolError as exc:
661
+ raise QpackEncoderStreamError('invalid QPACK encoder stream name reference') from exc
662
+
663
+ def _require_dynamic_entry(self, absolute_index: int, *, required_insert_count: int) -> tuple[bytes, bytes]:
664
+ if required_insert_count <= 0 or absolute_index >= required_insert_count:
665
+ raise QpackDecompressionFailed('invalid QPACK dynamic table reference')
666
+ try:
667
+ return self.dynamic_table.lookup_absolute(absolute_index)
668
+ except ProtocolError as exc:
669
+ raise QpackDecompressionFailed('invalid QPACK dynamic table reference') from exc
670
+
671
+ def _resolve_name(self, *, static: bool, base: int, index: int, post_base: bool = False, required_insert_count: int) -> bytes:
672
+ if static:
673
+ try:
674
+ name, _value = self.dynamic_table.lookup_static(index)
675
+ except ProtocolError as exc:
676
+ raise QpackDecompressionFailed('invalid QPACK static table index') from exc
677
+ return name
678
+ try:
679
+ absolute_index = (
680
+ self.dynamic_table.absolute_index_from_post_base(base, index)
681
+ if post_base
682
+ else self.dynamic_table.absolute_index_from_relative(base, index)
683
+ )
684
+ except ProtocolError as exc:
685
+ raise QpackDecompressionFailed('invalid QPACK dynamic name reference') from exc
686
+ name, _value = self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count)
687
+ return name
688
+
689
+ def receive_encoder_stream(self, data: bytes) -> None:
690
+ offset = 0
691
+ processed_inserts = 0
692
+ while offset < len(data):
693
+ first = data[offset]
694
+ if first & 0x80:
695
+ static = bool(first & 0x40)
696
+ name_index, offset = decode_qpack_integer(data, offset, 6)
697
+ name = self._lookup_encoder_stream_name(static=static, name_index=name_index)
698
+ try:
699
+ value, offset = decode_qpack_string(data, offset, 8)
700
+ self.dynamic_table.insert(name, value)
701
+ except ProtocolError as exc:
702
+ raise QpackEncoderStreamError('invalid QPACK encoder stream insertion') from exc
703
+ processed_inserts += 1
704
+ continue
705
+ if first & 0x40:
706
+ try:
707
+ name, offset = decode_qpack_string(data, offset, 6)
708
+ value, offset = decode_qpack_string(data, offset, 8)
709
+ self.dynamic_table.insert(name, value)
710
+ except ProtocolError as exc:
711
+ raise QpackEncoderStreamError('invalid QPACK encoder stream literal insertion') from exc
712
+ processed_inserts += 1
713
+ continue
714
+ if first & 0x20:
715
+ try:
716
+ capacity, offset = decode_qpack_integer(data, offset, 5)
717
+ self.dynamic_table.set_capacity(capacity)
718
+ except ProtocolError as exc:
719
+ raise QpackEncoderStreamError('invalid QPACK encoder stream capacity update') from exc
720
+ continue
721
+ try:
722
+ relative_index, offset = decode_qpack_integer(data, offset, 5)
723
+ self.dynamic_table.duplicate_relative(relative_index)
724
+ except ProtocolError as exc:
725
+ raise QpackEncoderStreamError('invalid QPACK duplicate instruction') from exc
726
+ processed_inserts += 1
727
+ if processed_inserts:
728
+ self.known_received_count += processed_inserts
729
+ self._pending_decoder_bytes.extend(encode_insert_count_increment(processed_inserts))
730
+
731
+ def decode_field_section(self, data: bytes, *, stream_id: int | None = 0) -> QpackFieldSection:
732
+ offset = 0
733
+ encoded_required, offset = decode_qpack_integer(data, offset, 8)
734
+ required_insert_count = self._decode_required_insert_count(encoded_required)
735
+ if required_insert_count > self.dynamic_table.insert_count:
736
+ self._mark_blocked(stream_id, required_insert_count)
737
+ raise QpackBlocked(required_insert_count)
738
+ if offset >= len(data):
739
+ raise QpackDecompressionFailed('truncated QPACK field section prefix')
740
+ sign = bool(data[offset] & 0x80)
741
+ delta_base, offset = decode_qpack_integer(data, offset, 7)
742
+ if sign:
743
+ if required_insert_count <= delta_base:
744
+ raise QpackDecompressionFailed('invalid QPACK base')
745
+ base = required_insert_count - delta_base - 1
746
+ else:
747
+ base = required_insert_count + delta_base
748
+ headers: list[tuple[bytes, bytes]] = []
749
+ used_dynamic = False
750
+ while offset < len(data):
751
+ first = data[offset]
752
+ if first & 0x80:
753
+ static = bool(first & 0x40)
754
+ index, offset = decode_qpack_integer(data, offset, 6)
755
+ if static:
756
+ try:
757
+ headers.append(self.dynamic_table.lookup_static(index))
758
+ except ProtocolError as exc:
759
+ raise QpackDecompressionFailed('invalid QPACK static table index') from exc
760
+ else:
761
+ try:
762
+ absolute_index = self.dynamic_table.absolute_index_from_relative(base, index)
763
+ except ProtocolError as exc:
764
+ raise QpackDecompressionFailed('invalid QPACK relative reference') from exc
765
+ headers.append(self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count))
766
+ used_dynamic = True
767
+ continue
768
+ if first & 0x40:
769
+ static = bool(first & 0x10)
770
+ name_index, offset = decode_qpack_integer(data, offset, 4)
771
+ name = self._resolve_name(
772
+ static=static,
773
+ base=base,
774
+ index=name_index,
775
+ post_base=False,
776
+ required_insert_count=required_insert_count,
777
+ )
778
+ value, offset = decode_qpack_string(data, offset, 8)
779
+ headers.append((name, value))
780
+ if not static:
781
+ used_dynamic = True
782
+ continue
783
+ if first & 0x20:
784
+ name, offset = decode_qpack_string(data, offset, 4)
785
+ value, offset = decode_qpack_string(data, offset, 8)
786
+ headers.append((name, value))
787
+ continue
788
+ if first & 0x10:
789
+ index, offset = decode_qpack_integer(data, offset, 4)
790
+ try:
791
+ absolute_index = self.dynamic_table.absolute_index_from_post_base(base, index)
792
+ except ProtocolError as exc:
793
+ raise QpackDecompressionFailed('invalid QPACK post-base reference') from exc
794
+ headers.append(self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count))
795
+ used_dynamic = True
796
+ continue
797
+ name_index, offset = decode_qpack_integer(data, offset, 3)
798
+ name = self._resolve_name(
799
+ static=False,
800
+ base=base,
801
+ index=name_index,
802
+ post_base=True,
803
+ required_insert_count=required_insert_count,
804
+ )
805
+ value, offset = decode_qpack_string(data, offset, 8)
806
+ headers.append((name, value))
807
+ used_dynamic = True
808
+ self._unmark_blocked(stream_id, required_insert_count)
809
+ if required_insert_count != 0 and stream_id is not None:
810
+ self._pending_decoder_bytes.extend(encode_section_ack(stream_id))
811
+ return QpackFieldSection(
812
+ required_insert_count=required_insert_count,
813
+ base=base,
814
+ headers=headers,
815
+ used_dynamic=used_dynamic,
816
+ )
817
+
818
+ def cancel_stream(self, stream_id: int) -> None:
819
+ blocked = self._blocked_requirements.pop(stream_id, None)
820
+ if not blocked:
821
+ return
822
+ if self.dynamic_table.maximum_capacity <= 0:
823
+ return
824
+ self._pending_decoder_bytes.extend(encode_stream_cancellation(stream_id))
825
+
826
+ def take_decoder_stream_data(self) -> bytes:
827
+ payload = bytes(self._pending_decoder_bytes)
828
+ self._pending_decoder_bytes.clear()
829
+ return payload
830
+
831
+
832
+ # Stateless helpers preserve the previous convenience API but now emit/parse the
833
+ # RFC 9204 field-section prefix as well.
834
+ def encode_field_line(name: bytes, value: bytes) -> bytes:
835
+ return QpackEncoder(max_table_capacity=0).encode_field_section([(name, value)])
836
+
837
+
838
+ def encode_field_section(headers: Iterable[tuple[bytes, bytes]]) -> bytes:
839
+ return QpackEncoder(max_table_capacity=0).encode_field_section(headers)
840
+
841
+
842
+ def decode_field_section(data: bytes) -> list[tuple[bytes, bytes]]:
843
+ return QpackDecoder(max_table_capacity=0).decode_field_section(data, stream_id=None).headers