hyperion-rb 1.6.2 → 2.11.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4768 -0
  3. data/README.md +222 -13
  4. data/ext/hyperion_h2_codec/Cargo.lock +7 -0
  5. data/ext/hyperion_h2_codec/Cargo.toml +33 -0
  6. data/ext/hyperion_h2_codec/extconf.rb +73 -0
  7. data/ext/hyperion_h2_codec/src/frames.rs +140 -0
  8. data/ext/hyperion_h2_codec/src/hpack/huffman.rs +161 -0
  9. data/ext/hyperion_h2_codec/src/hpack.rs +457 -0
  10. data/ext/hyperion_h2_codec/src/lib.rs +296 -0
  11. data/ext/hyperion_http/extconf.rb +28 -0
  12. data/ext/hyperion_http/h2_codec_glue.c +408 -0
  13. data/ext/hyperion_http/page_cache.c +1125 -0
  14. data/ext/hyperion_http/parser.c +473 -38
  15. data/ext/hyperion_http/sendfile.c +982 -0
  16. data/ext/hyperion_http/websocket.c +493 -0
  17. data/ext/hyperion_io_uring/Cargo.lock +33 -0
  18. data/ext/hyperion_io_uring/Cargo.toml +34 -0
  19. data/ext/hyperion_io_uring/extconf.rb +74 -0
  20. data/ext/hyperion_io_uring/src/lib.rs +316 -0
  21. data/lib/hyperion/adapter/rack.rb +370 -42
  22. data/lib/hyperion/admin_listener.rb +207 -0
  23. data/lib/hyperion/admin_middleware.rb +36 -7
  24. data/lib/hyperion/cli.rb +310 -11
  25. data/lib/hyperion/config.rb +440 -14
  26. data/lib/hyperion/connection.rb +679 -22
  27. data/lib/hyperion/deprecations.rb +81 -0
  28. data/lib/hyperion/dispatch_mode.rb +165 -0
  29. data/lib/hyperion/fiber_local.rb +75 -13
  30. data/lib/hyperion/h2_admission.rb +77 -0
  31. data/lib/hyperion/h2_codec.rb +499 -0
  32. data/lib/hyperion/http/page_cache.rb +122 -0
  33. data/lib/hyperion/http/sendfile.rb +696 -0
  34. data/lib/hyperion/http2/native_hpack_adapter.rb +70 -0
  35. data/lib/hyperion/http2_handler.rb +618 -19
  36. data/lib/hyperion/io_uring.rb +317 -0
  37. data/lib/hyperion/lint_wrapper_pool.rb +126 -0
  38. data/lib/hyperion/master.rb +96 -9
  39. data/lib/hyperion/metrics/path_templater.rb +68 -0
  40. data/lib/hyperion/metrics.rb +256 -0
  41. data/lib/hyperion/prometheus_exporter.rb +150 -0
  42. data/lib/hyperion/request.rb +13 -0
  43. data/lib/hyperion/response_writer.rb +477 -16
  44. data/lib/hyperion/runtime.rb +195 -0
  45. data/lib/hyperion/server/route_table.rb +179 -0
  46. data/lib/hyperion/server.rb +519 -55
  47. data/lib/hyperion/static_preload.rb +133 -0
  48. data/lib/hyperion/thread_pool.rb +61 -7
  49. data/lib/hyperion/tls.rb +343 -1
  50. data/lib/hyperion/version.rb +1 -1
  51. data/lib/hyperion/websocket/close_codes.rb +71 -0
  52. data/lib/hyperion/websocket/connection.rb +876 -0
  53. data/lib/hyperion/websocket/frame.rb +356 -0
  54. data/lib/hyperion/websocket/handshake.rb +525 -0
  55. data/lib/hyperion/worker.rb +111 -9
  56. data/lib/hyperion.rb +137 -3
  57. metadata +50 -1
@@ -0,0 +1,161 @@
1
+ //! HPACK static Huffman codec (RFC 7541 Appendix B).
2
+ //!
3
+ //! We implement bit-by-bit decoding because the table is small (256
4
+ //! entries + EOS, max 30-bit codes) and the per-call cost is
5
+ //! dominated by the ~tens-of-bytes of header strings, not the
6
+ //! per-bit branching.
7
+ //!
8
+ //! Encoding is NOT implemented here — the encoder emits raw octets
9
+ //! always (Huffman bit cleared on string literals). Decoding is
10
+ //! present because clients (browsers, h2load, curl) routinely Huffman-
11
+ //! encode their request headers.
12
+
13
+ use crate::frames::HpackError;
14
+
15
+ // (code, length) per byte 0..=255 + EOS at index 256. From RFC 7541 §B.
16
+ const TABLE: [(u32, u8); 257] = [
17
+ (0x1ff8, 13), (0x7fffd8, 23), (0xfffffe2, 28), (0xfffffe3, 28),
18
+ (0xfffffe4, 28), (0xfffffe5, 28), (0xfffffe6, 28), (0xfffffe7, 28),
19
+ (0xfffffe8, 28), (0xffffea, 24), (0x3ffffffc, 30), (0xfffffe9, 28),
20
+ (0xfffffea, 28), (0x3ffffffd, 30), (0xfffffeb, 28), (0xfffffec, 28),
21
+ (0xfffffed, 28), (0xfffffee, 28), (0xfffffef, 28), (0xffffff0, 28),
22
+ (0xffffff1, 28), (0xffffff2, 28), (0x3ffffffe, 30), (0xffffff3, 28),
23
+ (0xffffff4, 28), (0xffffff5, 28), (0xffffff6, 28), (0xffffff7, 28),
24
+ (0xffffff8, 28), (0xffffff9, 28), (0xffffffa, 28), (0xffffffb, 28),
25
+ (0x14, 6), (0x3f8, 10), (0x3f9, 10), (0xffa, 12),
26
+ (0x1ff9, 13), (0x15, 6), (0xf8, 8), (0x7fa, 11),
27
+ (0x3fa, 10), (0x3fb, 10), (0xf9, 8), (0x7fb, 11),
28
+ (0xfa, 8), (0x16, 6), (0x17, 6), (0x18, 6),
29
+ (0x0, 5), (0x1, 5), (0x2, 5), (0x19, 6),
30
+ (0x1a, 6), (0x1b, 6), (0x1c, 6), (0x1d, 6),
31
+ (0x1e, 6), (0x1f, 6), (0x5c, 7), (0xfb, 8),
32
+ (0x7ffc, 15), (0x20, 6), (0xffb, 12), (0x3fc, 10),
33
+ (0x1ffa, 13), (0x21, 6), (0x5d, 7), (0x5e, 7),
34
+ (0x5f, 7), (0x60, 7), (0x61, 7), (0x62, 7),
35
+ (0x63, 7), (0x64, 7), (0x65, 7), (0x66, 7),
36
+ (0x67, 7), (0x68, 7), (0x69, 7), (0x6a, 7),
37
+ (0x6b, 7), (0x6c, 7), (0x6d, 7), (0x6e, 7),
38
+ (0x6f, 7), (0x70, 7), (0x71, 7), (0x72, 7),
39
+ (0xfc, 8), (0x73, 7), (0xfd, 8), (0x1ffb, 13),
40
+ (0x7fff0, 19), (0x1ffc, 13), (0x3ffc, 14), (0x22, 6),
41
+ (0x7ffd, 15), (0x3, 5), (0x23, 6), (0x4, 5),
42
+ (0x24, 6), (0x5, 5), (0x25, 6), (0x26, 6),
43
+ (0x27, 6), (0x6, 5), (0x74, 7), (0x75, 7),
44
+ (0x28, 6), (0x29, 6), (0x2a, 6), (0x7, 5),
45
+ (0x2b, 6), (0x76, 7), (0x2c, 6), (0x8, 5),
46
+ (0x9, 5), (0x2d, 6), (0x77, 7), (0x78, 7),
47
+ (0x79, 7), (0x7a, 7), (0x7b, 7), (0x7ffe, 15),
48
+ (0x7fc, 11), (0x3ffd, 14), (0x1ffd, 13), (0xffffffc, 28),
49
+ (0xfffe6, 20), (0x3fffd2, 22), (0xfffe7, 20), (0xfffe8, 20),
50
+ (0x3fffd3, 22), (0x3fffd4, 22), (0x3fffd5, 22), (0x7fffd9, 23),
51
+ (0x3fffd6, 22), (0x7fffda, 23), (0x7fffdb, 23), (0x7fffdc, 23),
52
+ (0x7fffdd, 23), (0x7fffde, 23), (0xffffeb, 24), (0x7fffdf, 23),
53
+ (0xffffec, 24), (0xffffed, 24), (0x3fffd7, 22), (0x7fffe0, 23),
54
+ (0xffffee, 24), (0x7fffe1, 23), (0x7fffe2, 23), (0x7fffe3, 23),
55
+ (0x7fffe4, 23), (0x1fffdc, 21), (0x3fffd8, 22), (0x7fffe5, 23),
56
+ (0x3fffd9, 22), (0x7fffe6, 23), (0x7fffe7, 23), (0xffffef, 24),
57
+ (0x3fffda, 22), (0x1fffdd, 21), (0xfffe9, 20), (0x3fffdb, 22),
58
+ (0x3fffdc, 22), (0x7fffe8, 23), (0x7fffe9, 23), (0x1fffde, 21),
59
+ (0x7fffea, 23), (0x3fffdd, 22), (0x3fffde, 22), (0xfffff0, 24),
60
+ (0x1fffdf, 21), (0x3fffdf, 22), (0x7fffeb, 23), (0x7fffec, 23),
61
+ (0x1fffe0, 21), (0x1fffe1, 21), (0x3fffe0, 22), (0x1fffe2, 21),
62
+ (0x7fffed, 23), (0x3fffe1, 22), (0x7fffee, 23), (0x7fffef, 23),
63
+ (0xfffea, 20), (0x3fffe2, 22), (0x3fffe3, 22), (0x3fffe4, 22),
64
+ (0x7ffff0, 23), (0x3fffe5, 22), (0x3fffe6, 22), (0x7ffff1, 23),
65
+ (0x3ffffe0, 26), (0x3ffffe1, 26), (0xfffeb, 20), (0x7fff1, 19),
66
+ (0x3fffe7, 22), (0x7ffff2, 23), (0x3fffe8, 22), (0x1ffffec, 25),
67
+ (0x3ffffe2, 26), (0x3ffffe3, 26), (0x3ffffe4, 26), (0x7ffffde, 27),
68
+ (0x7ffffdf, 27), (0x3ffffe5, 26), (0xfffff1, 24), (0x1ffffed, 25),
69
+ (0x7fff2, 19), (0x1fffe3, 21), (0x3ffffe6, 26), (0x7ffffe0, 27),
70
+ (0x7ffffe1, 27), (0x3ffffe7, 26), (0x7ffffe2, 27), (0xfffff2, 24),
71
+ (0x1fffe4, 21), (0x1fffe5, 21), (0x3ffffe8, 26), (0x3ffffe9, 26),
72
+ (0xffffffd, 28), (0x7ffffe3, 27), (0x7ffffe4, 27), (0x7ffffe5, 27),
73
+ (0xfffec, 20), (0xfffff3, 24), (0xfffed, 20), (0x1fffe6, 21),
74
+ (0x3fffe9, 22), (0x1fffe7, 21), (0x1fffe8, 21), (0x7ffff3, 23),
75
+ (0x3fffea, 22), (0x3fffeb, 22), (0x1ffffee, 25), (0x1ffffef, 25),
76
+ (0xfffff4, 24), (0xfffff5, 24), (0x3ffffea, 26), (0x7ffff4, 23),
77
+ (0x3ffffeb, 26), (0x7ffffe6, 27), (0x3ffffec, 26), (0x3ffffed, 26),
78
+ (0x7ffffe7, 27), (0x7ffffe8, 27), (0x7ffffe9, 27), (0x7ffffea, 27),
79
+ (0x7ffffeb, 27), (0xffffffe, 28), (0x7ffffec, 27), (0x7ffffed, 27),
80
+ (0x7ffffee, 27), (0x7ffffef, 27), (0x7fffff0, 27), (0x3ffffee, 26),
81
+ (0x3fffffff, 30), // EOS at index 256
82
+ ];
83
+
84
+ // Bit-level streaming decoder. We walk the input bit-by-bit and look
85
+ // up the prefix in the static code table. The table has 257 entries
86
+ // max and codes are at most 30 bits long, so a linear search per
87
+ // extracted code is fine for the byte-scale string lengths HTTP
88
+ // headers carry.
89
+ pub fn decode(input: &[u8]) -> Result<Vec<u8>, HpackError> {
90
+ let mut out = Vec::with_capacity(input.len() * 2);
91
+ let mut acc: u64 = 0;
92
+ let mut acc_bits: u32 = 0;
93
+ let mut idx: usize = 0;
94
+
95
+ loop {
96
+ // Refill the accumulator until we have 30 bits or the input is
97
+ // exhausted.
98
+ while acc_bits < 30 && idx < input.len() {
99
+ acc = (acc << 8) | input[idx] as u64;
100
+ acc_bits += 8;
101
+ idx += 1;
102
+ }
103
+ if acc_bits == 0 {
104
+ break;
105
+ }
106
+
107
+ // Find a code that matches the high `len` bits of `acc`.
108
+ let mut matched: Option<(u8, u8)> = None;
109
+ for sym in 0u32..=255 {
110
+ let (code, len) = TABLE[sym as usize];
111
+ if (len as u32) > acc_bits {
112
+ continue;
113
+ }
114
+ let high = (acc >> (acc_bits - len as u32)) & ((1u64 << len) - 1);
115
+ if high == code as u64 {
116
+ matched = Some((sym as u8, len));
117
+ break;
118
+ }
119
+ }
120
+ match matched {
121
+ Some((sym, len)) => {
122
+ out.push(sym);
123
+ acc_bits -= len as u32;
124
+ acc &= (1u64 << acc_bits) - 1;
125
+ if acc_bits == 0 && idx >= input.len() {
126
+ break;
127
+ }
128
+ }
129
+ None => {
130
+ // Trailing bits at the end of the buffer that don't
131
+ // match any code: per RFC 7541 §5.2 they MUST be a
132
+ // prefix of the EOS symbol (all 1s). If so, we're
133
+ // done; otherwise the stream is malformed.
134
+ if idx >= input.len() && acc_bits < 8 {
135
+ let mask = (1u64 << acc_bits) - 1;
136
+ if acc == mask {
137
+ break;
138
+ }
139
+ }
140
+ return Err(HpackError::HuffmanInvalid);
141
+ }
142
+ }
143
+ }
144
+
145
+ Ok(out)
146
+ }
147
+
148
+ #[cfg(test)]
149
+ mod tests {
150
+ use super::*;
151
+
152
+ #[test]
153
+ fn decode_huffman_www_example_com() {
154
+ // RFC 7541 C.4.1 — value-only encoding of "www.example.com"
155
+ // Huffman-encoded:
156
+ // f1 e3 c2 e5 f2 3a 6b a0 ab 90 f4 ff
157
+ let bytes = [0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff];
158
+ let out = decode(&bytes).unwrap();
159
+ assert_eq!(out, b"www.example.com");
160
+ }
161
+ }
@@ -0,0 +1,457 @@
1
+ //! Pure-Rust HPACK (RFC 7541) encoder + decoder.
2
+ //!
3
+ //! Scope:
4
+ //! - Static table (61 entries from RFC 7541 Appendix A).
5
+ //! - Indexed Header Field encoding when name+value match a static slot.
6
+ //! - Indexed Header Field encoding when name+value match a *dynamic*
7
+ //! slot (Phase 10, RFC §3 Phase 6c) — closes the wire-bytes gap
8
+ //! with protocol-hpack on repeated headers, which is what makes
9
+ //! wiring the codec into the hot path actually faster.
10
+ //! - Indexed-Name + Literal Value with incremental indexing (`0x40`)
11
+ //! when name matches a static OR dynamic slot, AND for wholly-novel
12
+ //! names (so future repeats hit the indexed path).
13
+ //! - We deliberately do NOT emit Huffman-encoded strings — h2
14
+ //! conformance allows raw octets and the wire-size win on
15
+ //! server-side response headers is small (<10% on real workloads).
16
+ //!
17
+ //! The decoder is the operationally important half: clients (browsers,
18
+ //! curl, h2load) routinely Huffman-encode their request headers, so we
19
+ //! must round-trip those. RFC 7541 Appendix B Huffman codes are
20
+ //! embedded inline in `huffman.rs`.
21
+ //!
22
+ //! Dynamic table: maintained per-encoder/per-decoder, default 4096
23
+ //! bytes (RFC 7541 §6.5).
24
+
25
+ use crate::frames::HpackError;
26
+
27
+ mod huffman;
28
+
29
+ // ---- Static table (RFC 7541 Appendix A). 1-indexed in the spec; we
30
+ // store at offset 0 = entry 1 to keep encoder math readable.
31
+ const STATIC_TABLE: &[(&[u8], &[u8])] = &[
32
+ (b":authority", b""),
33
+ (b":method", b"GET"),
34
+ (b":method", b"POST"),
35
+ (b":path", b"/"),
36
+ (b":path", b"/index.html"),
37
+ (b":scheme", b"http"),
38
+ (b":scheme", b"https"),
39
+ (b":status", b"200"),
40
+ (b":status", b"204"),
41
+ (b":status", b"206"),
42
+ (b":status", b"304"),
43
+ (b":status", b"400"),
44
+ (b":status", b"404"),
45
+ (b":status", b"500"),
46
+ (b"accept-charset", b""),
47
+ (b"accept-encoding", b"gzip, deflate"),
48
+ (b"accept-language", b""),
49
+ (b"accept-ranges", b""),
50
+ (b"accept", b""),
51
+ (b"access-control-allow-origin", b""),
52
+ (b"age", b""),
53
+ (b"allow", b""),
54
+ (b"authorization", b""),
55
+ (b"cache-control", b""),
56
+ (b"content-disposition", b""),
57
+ (b"content-encoding", b""),
58
+ (b"content-language", b""),
59
+ (b"content-length", b""),
60
+ (b"content-location", b""),
61
+ (b"content-range", b""),
62
+ (b"content-type", b""),
63
+ (b"cookie", b""),
64
+ (b"date", b""),
65
+ (b"etag", b""),
66
+ (b"expect", b""),
67
+ (b"expires", b""),
68
+ (b"from", b""),
69
+ (b"host", b""),
70
+ (b"if-match", b""),
71
+ (b"if-modified-since", b""),
72
+ (b"if-none-match", b""),
73
+ (b"if-range", b""),
74
+ (b"if-unmodified-since", b""),
75
+ (b"last-modified", b""),
76
+ (b"link", b""),
77
+ (b"location", b""),
78
+ (b"max-forwards", b""),
79
+ (b"proxy-authenticate", b""),
80
+ (b"proxy-authorization", b""),
81
+ (b"range", b""),
82
+ (b"referer", b""),
83
+ (b"refresh", b""),
84
+ (b"retry-after", b""),
85
+ (b"server", b""),
86
+ (b"set-cookie", b""),
87
+ (b"strict-transport-security", b""),
88
+ (b"transfer-encoding", b""),
89
+ (b"user-agent", b""),
90
+ (b"vary", b""),
91
+ (b"via", b""),
92
+ (b"www-authenticate", b""),
93
+ ];
94
+
95
+ const STATIC_TABLE_LEN: usize = STATIC_TABLE.len(); // 61
96
+
97
+ // ---- Dynamic table entry. Owned bytes, sized per RFC 7541 §4.1.
98
+ #[derive(Clone)]
99
+ struct DynEntry {
100
+ name: Vec<u8>,
101
+ value: Vec<u8>,
102
+ }
103
+
104
+ impl DynEntry {
105
+ fn size(&self) -> usize {
106
+ // RFC 7541 §4.1: "size of an entry is the sum of its name's
107
+ // length in octets, its value's length in octets, and 32".
108
+ self.name.len() + self.value.len() + 32
109
+ }
110
+ }
111
+
112
+ #[derive(Default)]
113
+ struct DynTable {
114
+ entries: std::collections::VecDeque<DynEntry>,
115
+ size: usize,
116
+ max_size: usize,
117
+ }
118
+
119
+ impl DynTable {
120
+ fn new(max_size: usize) -> Self {
121
+ Self {
122
+ entries: std::collections::VecDeque::new(),
123
+ size: 0,
124
+ max_size,
125
+ }
126
+ }
127
+
128
+ fn set_max_size(&mut self, new_max: usize) {
129
+ self.max_size = new_max;
130
+ self.evict_to_fit(0);
131
+ }
132
+
133
+ fn add(&mut self, name: Vec<u8>, value: Vec<u8>) {
134
+ let entry = DynEntry { name, value };
135
+ if entry.size() > self.max_size {
136
+ // Spec: an entry larger than the dynamic table size simply
137
+ // empties the table.
138
+ self.entries.clear();
139
+ self.size = 0;
140
+ return;
141
+ }
142
+ self.evict_to_fit(entry.size());
143
+ self.size += entry.size();
144
+ self.entries.push_front(entry);
145
+ }
146
+
147
+ fn evict_to_fit(&mut self, incoming: usize) {
148
+ while self.size + incoming > self.max_size {
149
+ match self.entries.pop_back() {
150
+ Some(e) => self.size -= e.size(),
151
+ None => break,
152
+ }
153
+ }
154
+ }
155
+
156
+ /// Index in the combined table. `idx` is 1-based per RFC 7541.
157
+ /// Static is 1..=STATIC_TABLE_LEN; dynamic begins at STATIC_TABLE_LEN+1.
158
+ fn lookup(&self, idx: usize) -> Option<(&[u8], &[u8])> {
159
+ if idx == 0 {
160
+ return None;
161
+ }
162
+ if idx <= STATIC_TABLE_LEN {
163
+ let (n, v) = STATIC_TABLE[idx - 1];
164
+ Some((n, v))
165
+ } else {
166
+ let off = idx - STATIC_TABLE_LEN - 1;
167
+ self.entries.get(off).map(|e| (e.name.as_slice(), e.value.as_slice()))
168
+ }
169
+ }
170
+ }
171
+
172
+ // ---- Integer encoding (RFC 7541 §5.1).
173
+ fn encode_integer(prefix_bits: u8, value: usize, prefix_byte: u8, out: &mut Vec<u8>) {
174
+ let max_prefix = (1usize << prefix_bits) - 1;
175
+ if value < max_prefix {
176
+ out.push(prefix_byte | value as u8);
177
+ return;
178
+ }
179
+ out.push(prefix_byte | max_prefix as u8);
180
+ let mut v = value - max_prefix;
181
+ while v >= 128 {
182
+ out.push(((v & 0x7f) | 0x80) as u8);
183
+ v >>= 7;
184
+ }
185
+ out.push(v as u8);
186
+ }
187
+
188
+ fn decode_integer(input: &[u8], prefix_bits: u8) -> Result<(usize, usize), HpackError> {
189
+ if input.is_empty() {
190
+ return Err(HpackError::Truncated);
191
+ }
192
+ let max_prefix = (1usize << prefix_bits) - 1;
193
+ let mut value = (input[0] as usize) & max_prefix;
194
+ if value < max_prefix {
195
+ return Ok((value, 1));
196
+ }
197
+ let mut consumed = 1usize;
198
+ let mut shift = 0u32;
199
+ loop {
200
+ if consumed >= input.len() {
201
+ return Err(HpackError::Truncated);
202
+ }
203
+ let b = input[consumed];
204
+ consumed += 1;
205
+ value = value
206
+ .checked_add(((b & 0x7f) as usize) << shift)
207
+ .ok_or(HpackError::Overflow)?;
208
+ if (b & 0x80) == 0 {
209
+ return Ok((value, consumed));
210
+ }
211
+ shift += 7;
212
+ if shift > 63 {
213
+ return Err(HpackError::Overflow);
214
+ }
215
+ }
216
+ }
217
+
218
+ // ---- String encoding. Always emit raw (Huffman bit cleared).
219
+ fn encode_string(s: &[u8], out: &mut Vec<u8>) {
220
+ encode_integer(7, s.len(), 0x00, out);
221
+ out.extend_from_slice(s);
222
+ }
223
+
224
+ // Decode a string literal (Huffman-flagged or raw).
225
+ fn decode_string(input: &[u8]) -> Result<(Vec<u8>, usize), HpackError> {
226
+ if input.is_empty() {
227
+ return Err(HpackError::Truncated);
228
+ }
229
+ let huffman = (input[0] & 0x80) != 0;
230
+ let (len, hdr) = decode_integer(input, 7)?;
231
+ let total = hdr + len;
232
+ if input.len() < total {
233
+ return Err(HpackError::Truncated);
234
+ }
235
+ let raw = &input[hdr..total];
236
+ let bytes = if huffman {
237
+ huffman::decode(raw)?
238
+ } else {
239
+ raw.to_vec()
240
+ };
241
+ Ok((bytes, total))
242
+ }
243
+
244
+ // ---- Encoder.
245
+
246
+ pub struct Encoder {
247
+ dyn_table: DynTable,
248
+ /// Per-encoder scratch buffer reused across `encode_v2` calls.
249
+ /// fix-B (2.2.x): avoids one `Vec::with_capacity` allocation per
250
+ /// HEADERS frame on the Rust side, mirroring the Ruby-side scratch
251
+ /// buffer reuse. Cleared (length-zeroed, capacity preserved) at
252
+ /// the start of each encode call.
253
+ pub scratch: Vec<u8>,
254
+ }
255
+
256
+ impl Encoder {
257
+ pub fn new() -> Self {
258
+ Self {
259
+ dyn_table: DynTable::new(4096),
260
+ scratch: Vec::with_capacity(4096),
261
+ }
262
+ }
263
+
264
+ pub fn encode_header(&mut self, name: &[u8], value: &[u8], out: &mut Vec<u8>) {
265
+ // 1) full static-table match → 0x80 | index. Cheapest path; takes
266
+ // priority over the dynamic table for headers like `:method GET`
267
+ // where the static index is shorter or equal.
268
+ for (i, (n, v)) in STATIC_TABLE.iter().enumerate() {
269
+ if *n == name && *v == value {
270
+ encode_integer(7, i + 1, 0x80, out);
271
+ return;
272
+ }
273
+ }
274
+ // 2) full dynamic-table match → 0x80 | (STATIC_TABLE_LEN + 1 + offset).
275
+ // Phase 10 (Phase 6c) — without this branch the encoder never
276
+ // re-uses dyn-table inserts on repeated headers, so a stream
277
+ // that re-sends the same `cookie: …` collapses to a literal
278
+ // every time. Adding the search closes the wire-bytes gap with
279
+ // protocol-hpack's Ruby Compressor (which DOES search the
280
+ // dynamic table); fixes the regression where wire-mode native
281
+ // HPACK ran slower than fallback because of the missing
282
+ // compression.
283
+ for (off, e) in self.dyn_table.entries.iter().enumerate() {
284
+ if e.name == name && e.value == value {
285
+ let idx = STATIC_TABLE_LEN + 1 + off;
286
+ encode_integer(7, idx, 0x80, out);
287
+ return;
288
+ }
289
+ }
290
+ // 3) name-only static match → 0x40 | index, then literal value;
291
+ // insert into dyn table so future repeats hit branch (2).
292
+ for (i, (n, _)) in STATIC_TABLE.iter().enumerate() {
293
+ if *n == name {
294
+ encode_integer(6, i + 1, 0x40, out);
295
+ encode_string(value, out);
296
+ self.dyn_table.add(name.to_vec(), value.to_vec());
297
+ return;
298
+ }
299
+ }
300
+ // 4) name-only dynamic match → 0x40 | (STATIC_TABLE_LEN + 1 + off),
301
+ // literal value, insert new entry. Same shape as (3) but the
302
+ // name comes from the dyn table instead of the static one.
303
+ for (off, e) in self.dyn_table.entries.iter().enumerate() {
304
+ if e.name == name {
305
+ let idx = STATIC_TABLE_LEN + 1 + off;
306
+ encode_integer(6, idx, 0x40, out);
307
+ encode_string(value, out);
308
+ self.dyn_table.add(name.to_vec(), value.to_vec());
309
+ return;
310
+ }
311
+ }
312
+ // 5) Wholly novel name. Use literal-with-incremental-indexing
313
+ // (0x40 prefix, 6-bit zero index, name lit, value lit) so
314
+ // future repeats can collapse via branches (2)/(4). Previously
315
+ // emitted as "literal without indexing" (0x00 prefix), which
316
+ // is RFC-compliant but leaves zero compression on the table —
317
+ // since the wire path is now hot, paying one slot in the dyn
318
+ // table is the right tradeoff.
319
+ out.push(0x40);
320
+ encode_string(name, out);
321
+ encode_string(value, out);
322
+ self.dyn_table.add(name.to_vec(), value.to_vec());
323
+ }
324
+ }
325
+
326
+ // ---- Decoder.
327
+
328
+ pub struct Decoder {
329
+ dyn_table: DynTable,
330
+ }
331
+
332
+ impl Decoder {
333
+ pub fn new() -> Self {
334
+ Self {
335
+ dyn_table: DynTable::new(4096),
336
+ }
337
+ }
338
+
339
+ pub fn decode(&mut self, mut input: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>, HpackError> {
340
+ let mut out = Vec::new();
341
+ while !input.is_empty() {
342
+ let first = input[0];
343
+ if first & 0x80 != 0 {
344
+ // Indexed header field (RFC 7541 §6.1).
345
+ let (idx, used) = decode_integer(input, 7)?;
346
+ if idx == 0 {
347
+ return Err(HpackError::ZeroIndex);
348
+ }
349
+ let (n, v) = self
350
+ .dyn_table
351
+ .lookup(idx)
352
+ .ok_or(HpackError::BadIndex)?;
353
+ out.push((n.to_vec(), v.to_vec()));
354
+ input = &input[used..];
355
+ } else if first & 0xc0 == 0x40 {
356
+ // Literal with incremental indexing (§6.2.1).
357
+ let (idx, used) = decode_integer(input, 6)?;
358
+ input = &input[used..];
359
+ let name = if idx == 0 {
360
+ let (n, c) = decode_string(input)?;
361
+ input = &input[c..];
362
+ n
363
+ } else {
364
+ let (n, _) = self
365
+ .dyn_table
366
+ .lookup(idx)
367
+ .ok_or(HpackError::BadIndex)?;
368
+ n.to_vec()
369
+ };
370
+ let (value, c) = decode_string(input)?;
371
+ input = &input[c..];
372
+ self.dyn_table.add(name.clone(), value.clone());
373
+ out.push((name, value));
374
+ } else if first & 0xe0 == 0x20 {
375
+ // Dynamic table size update (§6.3).
376
+ let (new_max, used) = decode_integer(input, 5)?;
377
+ self.dyn_table.set_max_size(new_max);
378
+ input = &input[used..];
379
+ } else {
380
+ // 0x00 (literal without indexing) or 0x10 (never indexed)
381
+ // — same wire shape, just different routing semantics.
382
+ let (idx, used) = decode_integer(input, 4)?;
383
+ input = &input[used..];
384
+ let name = if idx == 0 {
385
+ let (n, c) = decode_string(input)?;
386
+ input = &input[c..];
387
+ n
388
+ } else {
389
+ let (n, _) = self
390
+ .dyn_table
391
+ .lookup(idx)
392
+ .ok_or(HpackError::BadIndex)?;
393
+ n.to_vec()
394
+ };
395
+ let (value, c) = decode_string(input)?;
396
+ input = &input[c..];
397
+ out.push((name, value));
398
+ }
399
+ }
400
+ Ok(out)
401
+ }
402
+ }
403
+
404
+ #[cfg(test)]
405
+ mod tests {
406
+ use super::*;
407
+
408
+ #[test]
409
+ fn integer_roundtrip_examples_rfc7541_c1() {
410
+ // C.1.1 — value 10, prefix 5 → 0b00001010
411
+ let mut buf = Vec::new();
412
+ encode_integer(5, 10, 0x00, &mut buf);
413
+ assert_eq!(buf, [0x0a]);
414
+ // C.1.2 — value 1337, prefix 5 → 0b00011111 0b10011010 0b00001010
415
+ buf.clear();
416
+ encode_integer(5, 1337, 0x00, &mut buf);
417
+ assert_eq!(buf, [0x1f, 0x9a, 0x0a]);
418
+
419
+ let (v, used) = decode_integer(&[0x1f, 0x9a, 0x0a], 5).unwrap();
420
+ assert_eq!(v, 1337);
421
+ assert_eq!(used, 3);
422
+ }
423
+
424
+ #[test]
425
+ fn round_trip_basic_request_headers() {
426
+ let mut enc = Encoder::new();
427
+ let mut buf = Vec::new();
428
+ enc.encode_header(b":method", b"GET", &mut buf);
429
+ enc.encode_header(b":path", b"/", &mut buf);
430
+ enc.encode_header(b":scheme", b"https", &mut buf);
431
+ enc.encode_header(b":authority", b"example.com", &mut buf);
432
+ enc.encode_header(b"accept", b"*/*", &mut buf);
433
+
434
+ let mut dec = Decoder::new();
435
+ let out = dec.decode(&buf).unwrap();
436
+ assert_eq!(out.len(), 5);
437
+ assert_eq!(out[0].0, b":method");
438
+ assert_eq!(out[0].1, b"GET");
439
+ assert_eq!(out[3].0, b":authority");
440
+ assert_eq!(out[3].1, b"example.com");
441
+ }
442
+
443
+ #[test]
444
+ fn round_trip_response_headers() {
445
+ let mut enc = Encoder::new();
446
+ let mut buf = Vec::new();
447
+ enc.encode_header(b":status", b"200", &mut buf);
448
+ enc.encode_header(b"content-type", b"text/plain", &mut buf);
449
+ enc.encode_header(b"content-length", b"42", &mut buf);
450
+
451
+ let mut dec = Decoder::new();
452
+ let out = dec.decode(&buf).unwrap();
453
+ assert_eq!(out.len(), 3);
454
+ assert_eq!(out[0], (b":status".to_vec(), b"200".to_vec()));
455
+ assert_eq!(out[1], (b"content-type".to_vec(), b"text/plain".to_vec()));
456
+ }
457
+ }