quico 0.3.0 → 0.7.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.
@@ -0,0 +1,520 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+
16
+ // ============================================================
17
+ // Field-compression primitives shared by HPACK and QPACK.
18
+ //
19
+ // QPACK (RFC 9204, HTTP/3) is a reworking of HPACK (RFC 7541, HTTP/2) for
20
+ // out-of-order delivery. The two differ in how the dynamic table is indexed
21
+ // and updated — but the *wire primitives* underneath are the same document:
22
+ //
23
+ // - Huffman code RFC 7541 Appendix B (QPACK cites it verbatim)
24
+ // - Prefixed integers RFC 7541 §5.1 (QPACK §4.1.1: identical)
25
+ // - String literals RFC 7541 §5.2 (QPACK §4.1.2: identical)
26
+ //
27
+ // Only the static tables differ (RFC 7541 Appendix A vs RFC 9204 Appendix A),
28
+ // and even those are built and searched the same way — hence the shared
29
+ // buildStaticLookup() below.
30
+ //
31
+ // The file is named for HPACK because HPACK is where these primitives are
32
+ // *defined*; QPACK imports them by reference. Nothing here is HTTP/2-specific.
33
+ // ============================================================
34
+
35
+
36
+ // ============================================================
37
+ // Huffman (RFC 7541 Appendix B)
38
+ //
39
+ // 257 entries: symbols 0..255 plus EOS at 256. EOS never appears in a
40
+ // well-formed encoding — it exists only as the source of the padding bit
41
+ // pattern (its code is all-ones), so decoding it is a protocol error.
42
+ // ============================================================
43
+
44
+ var HUFF_CODES = new Uint32Array([
45
+ 0x1ff8,0x7fffd8,0xfffffe2,0xfffffe3,0xfffffe4,0xfffffe5,0xfffffe6,0xfffffe7,
46
+ 0xfffffe8,0xffffea,0x3ffffffc,0xfffffe9,0xfffffea,0x3ffffffd,0xfffffeb,0xfffffec,
47
+ 0xfffffed,0xfffffee,0xfffffef,0xffffff0,0xffffff1,0xffffff2,0x3ffffffe,0xffffff3,
48
+ 0xffffff4,0xffffff5,0xffffff6,0xffffff7,0xffffff8,0xffffff9,0xffffffa,0xffffffb,
49
+ 0x14,0x3f8,0x3f9,0xffa,0x1ff9,0x15,0xf8,0x7fa,0x3fa,0x3fb,0xf9,0x7fb,0xfa,
50
+ 0x16,0x17,0x18,0x0,0x1,0x2,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x5c,0xfb,
51
+ 0x7ffc,0x20,0xffb,0x3fc,0x1ffa,0x21,0x5d,0x5e,0x5f,0x60,0x61,0x62,0x63,0x64,
52
+ 0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xfc,
53
+ 0x73,0xfd,0x1ffb,0x7fff0,0x1ffc,0x3ffc,0x22,0x7ffd,0x3,0x23,0x4,0x24,0x5,0x25,
54
+ 0x26,0x27,0x6,0x74,0x75,0x28,0x29,0x2a,0x7,0x2b,0x76,0x2c,0x8,0x9,0x2d,0x77,
55
+ 0x78,0x79,0x7a,0x7b,0x7ffe,0x7fc,0x3ffd,0x1ffd,0xffffffc,0xfffe6,0x3fffd2,
56
+ 0xfffe7,0xfffe8,0x3fffd3,0x3fffd4,0x3fffd5,0x7fffd9,0x3fffd6,0x7fffda,0x7fffdb,
57
+ 0x7fffdc,0x7fffdd,0x7fffde,0xffffeb,0x7fffdf,0xffffec,0xffffed,0x3fffd7,0x7fffe0,
58
+ 0xffffee,0x7fffe1,0x7fffe2,0x7fffe3,0x7fffe4,0x1fffdc,0x3fffd8,0x7fffe5,0x3fffd9,
59
+ 0x7fffe6,0x7fffe7,0xffffef,0x3fffda,0x1fffdd,0xfffe9,0x3fffdb,0x3fffdc,0x7fffe8,
60
+ 0x7fffe9,0x1fffde,0x7fffea,0x3fffdd,0x3fffde,0xfffff0,0x1fffdf,0x3fffdf,0x7fffeb,
61
+ 0x7fffec,0x1fffe0,0x1fffe1,0x3fffe0,0x1fffe2,0x7fffed,0x3fffe1,0x7fffee,0x7fffef,
62
+ 0xfffea,0x3fffe2,0x3fffe3,0x3fffe4,0x7ffff0,0x3fffe5,0x3fffe6,0x7ffff1,0x3ffffe0,
63
+ 0x3ffffe1,0xfffeb,0x7fff1,0x3fffe7,0x7ffff2,0x3fffe8,0x1ffffec,0x3ffffe2,0x3ffffe3,
64
+ 0x3ffffe4,0x7ffffde,0x7ffffdf,0x3ffffe5,0xfffff1,0x1ffffed,0x7fff2,0x1fffe3,
65
+ 0x3ffffe6,0x7ffffe0,0x7ffffe1,0x3ffffe7,0x7ffffe2,0xfffff2,0x1fffe4,0x1fffe5,
66
+ 0x3ffffe8,0x3ffffe9,0xffffffd,0x7ffffe3,0x7ffffe4,0x7ffffe5,0xfffec,0xfffff3,
67
+ 0xfffed,0x1fffe6,0x3fffe9,0x1fffe7,0x1fffe8,0x7ffff3,0x3fffea,0x3fffeb,0x1ffffee,
68
+ 0x1ffffef,0xfffff4,0xfffff5,0x3ffffea,0x7ffff4,0x3ffffeb,0x7ffffe6,0x3ffffec,
69
+ 0x3ffffed,0x7ffffe7,0x7ffffe8,0x7ffffe9,0x7ffffea,0x7ffffeb,0xffffffe,0x7ffffec,
70
+ 0x7ffffed,0x7ffffee,0x7ffffef,0x7fffff0,0x3ffffee,0x3fffffff
71
+ ]);
72
+
73
+ var HUFF_BITS = new Uint8Array([
74
+ 13,23,28,28,28,28,28,28,28,24,30,28,28,30,28,28,28,28,28,28,28,28,30,28,28,
75
+ 28,28,28,28,28,28,28,6,10,10,12,13,6,8,11,10,10,8,11,8,6,6,6,5,5,5,6,6,6,6,
76
+ 6,6,6,7,8,15,6,12,10,13,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,7,
77
+ 8,13,19,13,14,6,15,5,6,5,6,5,6,6,6,5,7,7,6,6,6,5,6,7,6,5,5,6,7,7,7,7,7,15,
78
+ 11,14,13,28,20,22,20,20,22,22,22,23,22,23,23,23,23,23,24,23,24,24,22,23,24,
79
+ 23,23,23,23,21,22,23,22,23,23,24,22,21,20,22,22,23,23,21,23,22,22,24,21,22,
80
+ 23,23,21,21,22,21,23,22,23,23,20,22,22,22,23,22,22,23,26,26,20,19,22,23,22,
81
+ 25,26,26,26,27,27,26,24,25,19,21,26,27,27,26,27,24,21,21,26,26,28,27,27,27,
82
+ 20,24,20,21,22,21,21,23,22,22,25,25,24,24,26,23,26,27,26,26,27,27,27,27,27,
83
+ 28,27,27,27,27,27,26,30
84
+ ]);
85
+
86
+ var HUFF_EOS = 256; // index of the EOS symbol; decoding it is an error
87
+
88
+ var _huffRoot = (function () {
89
+ var root = {};
90
+ for (var i = 0; i < HUFF_CODES.length; i++) {
91
+ var code = HUFF_CODES[i], bits = HUFF_BITS[i], node = root;
92
+ for (var j = bits - 1; j >= 0; j--) {
93
+ var bit = (code >> j) & 1;
94
+ if (!node[bit]) node[bit] = {};
95
+ node = node[bit];
96
+ }
97
+ node.sym = i;
98
+ }
99
+ return root;
100
+ })();
101
+
102
+
103
+ /**
104
+ * Decode a Huffman-coded byte string (RFC 7541 §5.2). Returns the raw
105
+ * Uint8Array, NOT a string.
106
+ *
107
+ * Field names and values are opaque octets on the wire, and the two callers
108
+ * disagree about how to turn them into text — so that decision stays with
109
+ * them. (QPACK in this codebase has always used TextDecoder/UTF-8; a stricter
110
+ * HPACK may prefer a lossless latin-1 mapping. Baking either one in here would
111
+ * silently change the other.)
112
+ *
113
+ * `strict` turns on the three padding checks the RFC mandates (§5.2): padding
114
+ * longer than 7 bits, padding that is not the EOS prefix (all ones), and an
115
+ * actual EOS symbol in the stream must all be treated as errors. They are off
116
+ * by default so that existing callers keep their current lenient behaviour;
117
+ * decoders that must answer a malformed peer with COMPRESSION_ERROR pass true.
118
+ */
119
+ function huffDecode(buf, strict) {
120
+ var out = [];
121
+ var node = _huffRoot;
122
+ var nbits = 0, current = 0;
123
+ var sincePadStart = 0; // bits consumed since the last complete symbol
124
+ var padIsOnes = true; // were all of those bits 1?
125
+
126
+ for (var i = 0; i < buf.length; i++) {
127
+ current = (current << 8) | buf[i];
128
+ nbits += 8;
129
+ while (nbits > 0) {
130
+ var bit = (current >> (nbits - 1)) & 1;
131
+ node = node[bit];
132
+ if (!node) throw new Error('Invalid Huffman code');
133
+ nbits--;
134
+ sincePadStart++;
135
+ if (bit === 0) padIsOnes = false;
136
+ if (node.sym !== undefined) {
137
+ if (strict && node.sym === HUFF_EOS) {
138
+ throw new Error('Huffman: EOS symbol in input');
139
+ }
140
+ out.push(node.sym);
141
+ node = _huffRoot;
142
+ sincePadStart = 0;
143
+ padIsOnes = true;
144
+ }
145
+ }
146
+ }
147
+
148
+ if (strict && node !== _huffRoot) {
149
+ // Trailing bits that did not complete a symbol are padding. They must be
150
+ // at most 7 bits and must be the most-significant bits of the EOS code.
151
+ if (sincePadStart > 7) throw new Error('Huffman: padding longer than 7 bits');
152
+ if (!padIsOnes) throw new Error('Huffman: padding is not the EOS prefix');
153
+ }
154
+
155
+ return Uint8Array.from(out);
156
+ }
157
+
158
+
159
+ /** Byte length huffEncode() would produce, without producing it. */
160
+ function huffEncodedLength(bytes) {
161
+ var bits = 0;
162
+ for (var i = 0; i < bytes.length; i++) bits += HUFF_BITS[bytes[i]];
163
+ return (bits + 7) >> 3;
164
+ }
165
+
166
+
167
+ // Powers of two as floats, used wherever a shift would exceed 32 bits.
168
+ // huffEncode accumulates up to 7 leftover bits plus one 30-bit code = 37 bits;
169
+ // readPrefixedInt walks a continuation chain whose shift reaches 7 bits per
170
+ // byte, so the table has to cover the full continuation limit below (an
171
+ // undersized table silently yields undefined → NaN, which readPrefixedInt then
172
+ // reports as "too large").
173
+ var _POW2 = (function () {
174
+ var p = new Float64Array(80);
175
+ for (var i = 0; i < 80; i++) p[i] = Math.pow(2, i);
176
+ return p;
177
+ })();
178
+
179
+
180
+ /**
181
+ * Huffman-encode a byte array (RFC 7541 §5.2). Pads the final byte with the
182
+ * most-significant bits of the EOS code, i.e. ones.
183
+ */
184
+ function huffEncode(bytes) {
185
+ var out = new Uint8Array(huffEncodedLength(bytes));
186
+ var pos = 0;
187
+ var cur = 0, nbits = 0;
188
+
189
+ for (var i = 0; i < bytes.length; i++) {
190
+ var sym = bytes[i];
191
+ cur = cur * _POW2[HUFF_BITS[sym]] + HUFF_CODES[sym];
192
+ nbits += HUFF_BITS[sym];
193
+ while (nbits >= 8) {
194
+ var shift = _POW2[nbits - 8];
195
+ out[pos++] = Math.floor(cur / shift) & 0xff;
196
+ cur = cur % shift;
197
+ nbits -= 8;
198
+ }
199
+ }
200
+
201
+ if (nbits > 0) {
202
+ var pad = 8 - nbits;
203
+ out[pos++] = ((cur * _POW2[pad]) + (_POW2[pad] - 1)) & 0xff;
204
+ }
205
+
206
+ return out;
207
+ }
208
+
209
+
210
+ // ============================================================
211
+ // Prefixed integers (RFC 7541 §5.1, QPACK §4.1.1)
212
+ // ============================================================
213
+
214
+ // A malicious peer can send an unbounded run of continuation bytes to make the
215
+ // decoder spin. RFC 7541 §5.1 notes that implementations must limit the value
216
+ // they will decode; ten continuation bytes already exceed 2^53, so anything
217
+ // longer is either an attack or a value we could not represent anyway.
218
+ var _MAX_CONTINUATION_BYTES = 10;
219
+
220
+ /**
221
+ * Read a prefixed integer at buf[pos].
222
+ * Returns { value, next } or null if the buffer ends mid-integer (truncated).
223
+ *
224
+ * Used by the QPACK header-block parser, the QPACK encoder-instruction parser,
225
+ * and the HPACK header-block parser — the encoding is byte-identical in all
226
+ * three.
227
+ */
228
+ function readPrefixedInt(buf, prefixBits, pos) {
229
+ if (pos >= buf.length) return null;
230
+ var max = (1 << prefixBits) - 1;
231
+ var value = buf[pos] & max;
232
+ pos++;
233
+ if (value < max) return { value: value, next: pos };
234
+
235
+ var m = 0;
236
+ var seen = 0;
237
+ while (pos < buf.length) {
238
+ var byte = buf[pos++];
239
+ // Multiplication rather than `<< m`: m reaches 28+ on large values, where
240
+ // the 32-bit shift wraps and silently produces a wrong (often negative)
241
+ // integer.
242
+ value += (byte & 0x7f) * _POW2[m];
243
+ if ((byte & 0x80) === 0) {
244
+ if (!isFinite(value) || value > Number.MAX_SAFE_INTEGER) {
245
+ throw new Error('Prefixed integer too large');
246
+ }
247
+ return { value: value, next: pos };
248
+ }
249
+ m += 7;
250
+ if (++seen > _MAX_CONTINUATION_BYTES) {
251
+ throw new Error('Prefixed integer too long');
252
+ }
253
+ }
254
+ return null; // truncated mid-integer
255
+ }
256
+
257
+
258
+ /**
259
+ * Encode a prefixed integer (RFC 7541 §5.1). Returns a plain Array of bytes;
260
+ * the caller ORs its opcode bits into byte 0, which is why the high bits above
261
+ * the prefix are left clear here.
262
+ */
263
+ function encodePrefixedInt(value, prefixBits) {
264
+ var max = (1 << prefixBits) - 1;
265
+ if (value < max) return [value];
266
+ var bytes = [max];
267
+ value -= max;
268
+ while (value >= 128) {
269
+ bytes.push((value % 128) | 0x80);
270
+ value = Math.floor(value / 128);
271
+ }
272
+ bytes.push(value);
273
+ return bytes;
274
+ }
275
+
276
+
277
+ // ============================================================
278
+ // String literals (RFC 7541 §5.2, QPACK §4.1.2)
279
+ //
280
+ // Layout: one bit H (Huffman?), then a (prefixBits)-wide prefixed integer
281
+ // holding the byte length, then that many bytes. H sits immediately above the
282
+ // length prefix, so its mask is 1 << prefixBits — 0x80 for the usual 7-bit
283
+ // prefix, 0x08 for the 3-bit prefix QPACK uses for literal names.
284
+ // ============================================================
285
+
286
+ /**
287
+ * Decode a string literal at buf[pos].
288
+ * Returns { bytes, huffman, next }, or null if the literal is truncated.
289
+ *
290
+ * `bytes` is a Uint8Array for the same reason huffDecode returns one: the text
291
+ * codec is the caller's decision.
292
+ */
293
+ function decodeStringLiteral(buf, prefixBits, pos, strict) {
294
+ if (pos >= buf.length) return null;
295
+ var huffman = (buf[pos] & (1 << prefixBits)) !== 0;
296
+ var len = readPrefixedInt(buf, prefixBits, pos);
297
+ if (!len) return null;
298
+ pos = len.next;
299
+ if (pos + len.value > buf.length) return null; // truncated payload
300
+ var raw = buf.subarray(pos, pos + len.value);
301
+ pos += len.value;
302
+ return {
303
+ bytes: huffman ? huffDecode(raw, strict) : raw,
304
+ huffman: huffman,
305
+ next: pos
306
+ };
307
+ }
308
+
309
+
310
+ /**
311
+ * Encode a string literal. Returns a plain Array of bytes whose byte 0 already
312
+ * carries the H bit; the caller ORs in the opcode bits above it.
313
+ *
314
+ * `input` may be a Uint8Array (used as-is) or a string (UTF-8 encoded).
315
+ *
316
+ * useHuffman:
317
+ * undefined — Huffman only when it actually shrinks the string. This is what
318
+ * browsers and nghttp2 do: Huffman can *grow* a run of unusual
319
+ * bytes, and an encoder is free to send either form.
320
+ * true/false — force. `false` is what QPACK here has always done; leaving it
321
+ * that way keeps the bytes on the wire unchanged.
322
+ */
323
+ function encodeStringLiteral(input, prefixBits, useHuffman) {
324
+ var raw = (typeof input === 'string') ? _utf8(input) : input;
325
+ var body = raw;
326
+ var huffman = false;
327
+
328
+ if (useHuffman !== false) {
329
+ var encoded = huffEncode(raw);
330
+ if (useHuffman === true || encoded.length < raw.length) {
331
+ body = encoded;
332
+ huffman = true;
333
+ }
334
+ }
335
+
336
+ var out = encodePrefixedInt(body.length, prefixBits);
337
+ if (huffman) out[0] |= (1 << prefixBits);
338
+ for (var i = 0; i < body.length; i++) out.push(body[i]);
339
+ return out;
340
+ }
341
+
342
+
343
+ // ============================================================
344
+ // Static tables
345
+ // ============================================================
346
+
347
+ /**
348
+ * HPACK static table — RFC 7541 Appendix A. 61 entries, indexed from 1.
349
+ * Stored 0-based here; buildStaticLookup(HPACK_STATIC, 1) yields wire indices.
350
+ */
351
+ var HPACK_STATIC = [
352
+ [':authority',''], [':method','GET'], [':method','POST'], [':path','/'],
353
+ [':path','/index.html'], [':scheme','http'], [':scheme','https'],
354
+ [':status','200'], [':status','204'], [':status','206'], [':status','304'],
355
+ [':status','400'], [':status','404'], [':status','500'],
356
+ ['accept-charset',''], ['accept-encoding','gzip, deflate'],
357
+ ['accept-language',''], ['accept-ranges',''], ['accept',''],
358
+ ['access-control-allow-origin',''], ['age',''], ['allow',''],
359
+ ['authorization',''], ['cache-control',''], ['content-disposition',''],
360
+ ['content-encoding',''], ['content-language',''], ['content-length',''],
361
+ ['content-location',''], ['content-range',''], ['content-type',''],
362
+ ['cookie',''], ['date',''], ['etag',''], ['expect',''], ['expires',''],
363
+ ['from',''], ['host',''], ['if-match',''], ['if-modified-since',''],
364
+ ['if-none-match',''], ['if-range',''], ['if-unmodified-since',''],
365
+ ['last-modified',''], ['link',''], ['location',''], ['max-forwards',''],
366
+ ['proxy-authenticate',''], ['proxy-authorization',''], ['range',''],
367
+ ['referer',''], ['refresh',''], ['retry-after',''], ['server',''],
368
+ ['set-cookie',''], ['strict-transport-security',''],
369
+ ['transfer-encoding',''], ['user-agent',''], ['vary',''], ['via',''],
370
+ ['www-authenticate','']
371
+ ];
372
+
373
+ /**
374
+ * QPACK static table — RFC 9204 Appendix A. 99 entries, indexed from 0.
375
+ * Unrelated to the HPACK table above: different membership, different order.
376
+ */
377
+ var QPACK_STATIC = [
378
+ [':authority',''], [':path','/'], ['age','0'], ['content-disposition',''],
379
+ ['content-length','0'], ['cookie',''], ['date',''], ['etag',''],
380
+ ['if-modified-since',''], ['if-none-match',''], ['last-modified',''],
381
+ ['link',''], ['location',''], ['referer',''], ['set-cookie',''],
382
+ [':method','CONNECT'], [':method','DELETE'], [':method','GET'],
383
+ [':method','HEAD'], [':method','OPTIONS'], [':method','POST'],
384
+ [':method','PUT'], [':scheme','http'], [':scheme','https'],
385
+ [':status','103'], [':status','200'], [':status','304'],
386
+ [':status','404'], [':status','503'], ['accept','*/*'],
387
+ ['accept','application/dns-message'], ['accept-encoding','gzip, deflate, br'],
388
+ ['accept-ranges','bytes'], ['access-control-allow-headers','cache-control'],
389
+ ['access-control-allow-headers','content-type'],
390
+ ['access-control-allow-origin','*'], ['cache-control','max-age=0'],
391
+ ['cache-control','max-age=2592000'], ['cache-control','max-age=604800'],
392
+ ['cache-control','no-cache'], ['cache-control','no-store'],
393
+ ['cache-control','public, max-age=31536000'], ['content-encoding','br'],
394
+ ['content-encoding','gzip'], ['content-type','application/dns-message'],
395
+ ['content-type','application/javascript'], ['content-type','application/json'],
396
+ ['content-type','application/x-www-form-urlencoded'],
397
+ ['content-type','image/gif'], ['content-type','image/jpeg'],
398
+ ['content-type','image/png'], ['content-type','text/css'],
399
+ ['content-type','text/html; charset=utf-8'], ['content-type','text/plain'],
400
+ ['content-type','text/plain;charset=utf-8'], ['range','bytes=0-'],
401
+ ['strict-transport-security','max-age=31536000'],
402
+ ['strict-transport-security','max-age=31536000; includesubdomains'],
403
+ ['strict-transport-security','max-age=31536000; includesubdomains; preload'],
404
+ ['vary','accept-encoding'], ['vary','origin'],
405
+ ['x-content-type-options','nosniff'], ['x-xss-protection','1; mode=block'],
406
+ [':status','100'], [':status','204'], [':status','206'], [':status','302'],
407
+ [':status','400'], [':status','403'], [':status','421'], [':status','425'],
408
+ [':status','500'], ['accept-language',''],
409
+ ['access-control-allow-credentials','FALSE'],
410
+ ['access-control-allow-credentials','TRUE'],
411
+ ['access-control-allow-headers','*'],
412
+ ['access-control-allow-methods','get'],
413
+ ['access-control-allow-methods','get, post, options'],
414
+ ['access-control-allow-methods','options'],
415
+ ['access-control-expose-headers','content-length'],
416
+ ['access-control-request-headers','content-type'],
417
+ ['access-control-request-method','get'],
418
+ ['access-control-request-method','post'], ['alt-svc','clear'],
419
+ ['authorization',''],
420
+ ['content-security-policy',"script-src 'none'; object-src 'none'; base-uri 'none'"],
421
+ ['early-data','1'], ['expect-ct',''], ['forwarded',''], ['if-range',''],
422
+ ['origin',''], ['purpose','prefetch'], ['server',''],
423
+ ['timing-allow-origin','*'], ['upgrade-insecure-requests','1'],
424
+ ['user-agent',''], ['x-forwarded-for',''],
425
+ ['x-frame-options','deny'], ['x-frame-options','sameorigin']
426
+ ];
427
+
428
+
429
+ /**
430
+ * Build the two lookup maps an encoder needs for a static table:
431
+ * full: "name\0value" → index (exact entry → single indexed field)
432
+ * name: "name" → index (name only → literal with name reference)
433
+ *
434
+ * `offset` is added to every index: 0 for QPACK (indexed from 0), 1 for HPACK
435
+ * (indexed from 1). Earlier entries win, so the lowest index is kept for a
436
+ * duplicated name — smaller on the wire, and it matches what other encoders do.
437
+ */
438
+ function buildStaticLookup(table, offset) {
439
+ offset = offset || 0;
440
+ var full = {}, name = {};
441
+ for (var i = 0; i < table.length; i++) {
442
+ var key = table[i][0] + '\0' + table[i][1];
443
+ if (!(key in full)) full[key] = i + offset;
444
+ if (!(table[i][0] in name)) name[table[i][0]] = i + offset;
445
+ }
446
+ return { full: full, name: name };
447
+ }
448
+
449
+
450
+ // ============================================================
451
+ // Text helpers
452
+ //
453
+ // Offered, not imposed — see the note on huffDecode. TextDecoder('utf-8')
454
+ // replaces any non-UTF-8 byte with U+FFFD and loses it; latin-1 is a lossless
455
+ // byte↔char mapping, which is also how node:http surfaces header values. A new
456
+ // decoder should prefer bytesToLatin1; QPACK below stays on UTF-8 because that
457
+ // is what it has always done and this refactor is not the place to change it.
458
+ // ============================================================
459
+
460
+ function bytesToLatin1(bytes) {
461
+ var s = '';
462
+ // Chunked to stay clear of the argument-count limit on very long values.
463
+ for (var i = 0; i < bytes.length; i += 4096) {
464
+ s += String.fromCharCode.apply(null, Array.prototype.slice.call(bytes, i, i + 4096));
465
+ }
466
+ return s;
467
+ }
468
+
469
+ function _utf8(str) {
470
+ return new TextEncoder().encode(str);
471
+ }
472
+
473
+
474
+ // ============================================================
475
+ // Dynamic table entry size
476
+ //
477
+ // RFC 7541 §4.1 and RFC 9204 §3.2.1 define it identically: the name, the
478
+ // value, and 32 bytes of assumed per-entry overhead. The constant stands in
479
+ // for the bookkeeping a real implementation carries, and BOTH PEERS MUST
480
+ // AGREE ON IT — a table sized any other way drifts apart from the peer's and
481
+ // every index after the first eviction resolves to the wrong field.
482
+ //
483
+ // In bytes, not characters. Sizing by string length overflows the table on
484
+ // any non-ASCII header value, and does so silently.
485
+ // ============================================================
486
+
487
+ var ENTRY_OVERHEAD = 32;
488
+
489
+ function entrySize(name, value) {
490
+ return Buffer.byteLength(name, 'utf8') + Buffer.byteLength(value, 'utf8') + ENTRY_OVERHEAD;
491
+ }
492
+
493
+
494
+ export {
495
+ ENTRY_OVERHEAD,
496
+ entrySize,
497
+ // Huffman
498
+ HUFF_CODES,
499
+ HUFF_BITS,
500
+ HUFF_EOS,
501
+ huffDecode,
502
+ huffEncode,
503
+ huffEncodedLength,
504
+
505
+ // Prefixed integers
506
+ readPrefixedInt,
507
+ encodePrefixedInt,
508
+
509
+ // String literals
510
+ decodeStringLiteral,
511
+ encodeStringLiteral,
512
+
513
+ // Static tables
514
+ HPACK_STATIC,
515
+ QPACK_STATIC,
516
+ buildStaticLookup,
517
+
518
+ // Text
519
+ bytesToLatin1
520
+ };