h2lean 0.0.0-stage → 0.1.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.
- package/LICENSE +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
package/lib/hpack.js
ADDED
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
// HPACK codec (RFC 7541) for h2lite.
|
|
2
|
+
//
|
|
3
|
+
// Deliberate choices, all CPU-driven:
|
|
4
|
+
// * field names are interned, so a repeated name costs no new string and the
|
|
5
|
+
// header object keeps a stable hidden class
|
|
6
|
+
// * the encoder keeps a dynamic table (RFC 7541 2.3.2), so a repeated field line
|
|
7
|
+
// costs one indexed octet on the wire instead of its full literal bytes -- and it
|
|
8
|
+
// inserts a value only once it has seen it twice, so a header that is unique to
|
|
9
|
+
// every response never enters the table and never disturbs the caches
|
|
10
|
+
// * the encoder caches the *encoded bytes* of every (name, value, neverIndex)
|
|
11
|
+
// triple it has produced: a repeated header line becomes a Map lookup plus a
|
|
12
|
+
// memcpy, with no integer or Huffman work at all. Those bytes can name a dynamic
|
|
13
|
+
// index, so the cache is dropped whenever the table changes -- after warm-up the
|
|
14
|
+
// table stops changing and the cache stops being cleared
|
|
15
|
+
// * the cache is keyed in two levels -- the name, then the value -- and not by one
|
|
16
|
+
// composite 'name\0value' string. The composite key would have to be built for
|
|
17
|
+
// every field line of every response and hashed by the Map on the way in
|
|
18
|
+
// (OPTIMIZATION.md item 11). That is what a header block that cannot be cached as
|
|
19
|
+
// a whole costs per field line, and a block cannot be cached whenever the response
|
|
20
|
+
// headers are not the same object twice -- the compat API, and any response that
|
|
21
|
+
// echoes a request id
|
|
22
|
+
// * the two forms that are deliberately NOT cached -- the first sighting of a pair
|
|
23
|
+
// and the insertion that follows it -- are written straight into the destination
|
|
24
|
+
// buffer. They used to be built in a buffer of their own and copied in, which is
|
|
25
|
+
// one allocation and one memcpy per line, and the lines that take those forms are
|
|
26
|
+
// exactly the ones a server with a unique value in every response pays for
|
|
27
|
+
// * the encoded form of a NAME is cached, separately from the pair: a name that is
|
|
28
|
+
// in neither table is written in full, and a server that echoes a request id
|
|
29
|
+
// writes the same name with a new value every response, so the name's octets
|
|
30
|
+
// were Huffman-coded again for every one of those values
|
|
31
|
+
// * the "seen exactly once" set is bounded per name (ONCE_PER_NAME = 64): a name
|
|
32
|
+
// whose values never repeat -- the request id a server echoes in every response --
|
|
33
|
+
// stops being remembered, because the check costs a hash of a fresh value string
|
|
34
|
+
// on every response and the second sighting the set exists for never comes
|
|
35
|
+
//
|
|
36
|
+
// The encoder therefore holds per-connection state and must not be shared between
|
|
37
|
+
// sessions: the table has to match the one the peer's decoder builds from the same
|
|
38
|
+
// stream. lib/session.js owns one Encoder per session, exactly as it owns a Decoder.
|
|
39
|
+
//
|
|
40
|
+
// Where this differs from nghttp2, which is what node:http2 embeds, and why. Read from
|
|
41
|
+
// lib/nghttp2_hd.c v1.70.0 (hd_deflate_decide_indexing(), deflate_nv()); the differences
|
|
42
|
+
// were decided on measurements in bench/hpack-churn.js, not on preference:
|
|
43
|
+
// * insert on the second sighting (nghttp2 inserts on the first). A value that appears
|
|
44
|
+
// once per response is table poison -- it evicts a stable entry to be referenced by a
|
|
45
|
+
// block that will never use it again. Cost: one response warms up later than node's;
|
|
46
|
+
// in steady state the wire cost matches node's exactly (OPTIMIZATION.md item 11).
|
|
47
|
+
// * a name-based never-index list is NOT copied (:path, etag, location, age,
|
|
48
|
+
// content-length, if-*). An ETag is per-resource, not per-response, so indexing it is
|
|
49
|
+
// the whole point; copying that list measured worse on both axes (OPTIMIZATION.md
|
|
50
|
+
// item 27). The size half of the same rule is copied: nothing is inserted that needs
|
|
51
|
+
// more than three quarters of the table.
|
|
52
|
+
// * REINSERT_MAX: if the working set keeps evicting entries that are still wanted, the
|
|
53
|
+
// session stops inserting altogether. nghttp2 has no such thing because an eviction
|
|
54
|
+
// there costs a memcpy; here it costs both byte caches on every response.
|
|
55
|
+
// * ONCE_PER_NAME: a name that has produced 64 distinct values without any of them
|
|
56
|
+
// coming back stops being remembered, so it never inserts at all. The rule above
|
|
57
|
+
// would have inserted the pair on a second sighting, and for a header that is unique
|
|
58
|
+
// to every response that sighting never comes -- but a name whose values do repeat
|
|
59
|
+
// after 64 distinct ones pays for it: it stays a literal instead of being indexed.
|
|
60
|
+
// The per-line cost of the check: OPTIMIZATION.md item 11.
|
|
61
|
+
'use strict';
|
|
62
|
+
|
|
63
|
+
const { STATIC, HUFF_BITS, HUFF_CODES } = require('./hpack-data');
|
|
64
|
+
|
|
65
|
+
const STATIC_LEN = STATIC.length;
|
|
66
|
+
const EOS = 256;
|
|
67
|
+
const MAX_LITERAL = 1 << 20; // hard cap on one field name or value
|
|
68
|
+
const CACHE_LIMIT = 8192;
|
|
69
|
+
|
|
70
|
+
class HPackError extends Error {
|
|
71
|
+
constructor(msg) {
|
|
72
|
+
super(msg);
|
|
73
|
+
this.name = 'HPackError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- static table lookups -----------------------------------------------------
|
|
78
|
+
const FULL_INDEX = new Map(); // 'name\0value' -> 1-based static index
|
|
79
|
+
const NAME_INDEX = new Map(); // 'name' -> first 1-based index with that name
|
|
80
|
+
// The same lookup in two levels: the name, then the value. The encoder asks it on
|
|
81
|
+
// every field line it has to build, and the composite key it would otherwise build
|
|
82
|
+
// for the one-level Map is what this table exists to avoid (see the header).
|
|
83
|
+
const STATIC_FULL = new Map(); // name -> Map<value, 1-based static index>
|
|
84
|
+
for (let i = 0; i < STATIC_LEN; i++) {
|
|
85
|
+
const [n, v] = STATIC[i];
|
|
86
|
+
const k = `${n}\0${v}`;
|
|
87
|
+
if (!FULL_INDEX.has(k)) {
|
|
88
|
+
FULL_INDEX.set(k, i + 1);
|
|
89
|
+
}
|
|
90
|
+
if (!NAME_INDEX.has(n)) {
|
|
91
|
+
NAME_INDEX.set(n, i + 1);
|
|
92
|
+
}
|
|
93
|
+
let inner = STATIC_FULL.get(n);
|
|
94
|
+
if (inner === undefined) {
|
|
95
|
+
inner = new Map();
|
|
96
|
+
STATIC_FULL.set(n, inner);
|
|
97
|
+
}
|
|
98
|
+
if (!inner.has(v)) {
|
|
99
|
+
inner.set(v, i + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- Huffman decode trie ------------------------------------------------------
|
|
104
|
+
const TR_LEAF = new Int32Array(1 + 257 * 30).fill(-1);
|
|
105
|
+
const TR_LEFT = new Int32Array(1 + 257 * 30);
|
|
106
|
+
const TR_RIGHT = new Int32Array(1 + 257 * 30);
|
|
107
|
+
{
|
|
108
|
+
let nodes = 1;
|
|
109
|
+
for (let s = 0; s <= EOS; s++) {
|
|
110
|
+
let node = 0;
|
|
111
|
+
for (let i = HUFF_BITS[s] - 1; i >= 0; i--) {
|
|
112
|
+
if ((HUFF_CODES[s] >>> i) & 1) {
|
|
113
|
+
if (TR_RIGHT[node] === 0) {
|
|
114
|
+
TR_RIGHT[node] = nodes++;
|
|
115
|
+
}
|
|
116
|
+
node = TR_RIGHT[node];
|
|
117
|
+
} else {
|
|
118
|
+
if (TR_LEFT[node] === 0) {
|
|
119
|
+
TR_LEFT[node] = nodes++;
|
|
120
|
+
}
|
|
121
|
+
node = TR_LEFT[node];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
TR_LEAF[node] = s;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- integer codec (RFC 7541 §5.1) --------------------------------------------
|
|
129
|
+
// The result lands in RI/RP so that no tuple is allocated per call.
|
|
130
|
+
let RI = 0;
|
|
131
|
+
let RP = 0;
|
|
132
|
+
|
|
133
|
+
function readInt(buf, p, bits, end) {
|
|
134
|
+
const mask = (1 << bits) - 1;
|
|
135
|
+
if (p >= end) {
|
|
136
|
+
throw new HPackError('hpack: truncated integer');
|
|
137
|
+
}
|
|
138
|
+
let v = buf[p++] & mask;
|
|
139
|
+
if (v < mask) {
|
|
140
|
+
RI = v; RP = p; return;
|
|
141
|
+
}
|
|
142
|
+
// RFC 7541 5.1: the first continuation octet carries the low 7 bits (weight 1),
|
|
143
|
+
// each following one weighs 128 times more.
|
|
144
|
+
let mul = 1;
|
|
145
|
+
while (p < end) {
|
|
146
|
+
const b = buf[p++];
|
|
147
|
+
v += (b & 0x7f) * mul;
|
|
148
|
+
if (v > 0x7fffffff) {
|
|
149
|
+
throw new HPackError('hpack: integer too large');
|
|
150
|
+
}
|
|
151
|
+
mul *= 128;
|
|
152
|
+
if ((b & 0x80) === 0) {
|
|
153
|
+
RI = v; RP = p; return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new HPackError('hpack: truncated integer');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeInt(dst, off, value, bits, firstHigh) {
|
|
160
|
+
const max = (1 << bits) - 1;
|
|
161
|
+
if (value < max) {
|
|
162
|
+
dst[off++] = firstHigh | value;
|
|
163
|
+
return off;
|
|
164
|
+
}
|
|
165
|
+
dst[off++] = firstHigh | max;
|
|
166
|
+
let v = value - max;
|
|
167
|
+
while (v >= 128) {
|
|
168
|
+
dst[off++] = (v & 0x7f) | 0x80;
|
|
169
|
+
v >>>= 7;
|
|
170
|
+
}
|
|
171
|
+
dst[off++] = v;
|
|
172
|
+
return off;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- Huffman ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function huffBitLen(buf, start, end) {
|
|
178
|
+
let bits = 0;
|
|
179
|
+
for (let i = start; i < end; i++) {
|
|
180
|
+
bits += HUFF_BITS[buf[i]];
|
|
181
|
+
}
|
|
182
|
+
return bits;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The same sum for a string, without building its bytes: latin1 truncates every
|
|
186
|
+
// code unit to its low byte, so the byte of a character is its code unit & 0xff.
|
|
187
|
+
// Building the bytes first is the allocation this avoids (OPTIMIZATION.md item 12).
|
|
188
|
+
function huffBitsOf(s) {
|
|
189
|
+
let bits = 0;
|
|
190
|
+
for (let i = 0; i < s.length; i++) {
|
|
191
|
+
bits += HUFF_BITS[s.charCodeAt(i) & 0xff];
|
|
192
|
+
}
|
|
193
|
+
return bits;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The octets an integer takes in the HPACK integer encoding (RFC 7541 5.1).
|
|
197
|
+
function intLen(value, bits) {
|
|
198
|
+
const max = (1 << bits) - 1;
|
|
199
|
+
if (value < max) {
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
let v = value - max;
|
|
203
|
+
let n = 2;
|
|
204
|
+
while (v >= 128) {
|
|
205
|
+
v >>>= 7;
|
|
206
|
+
n++;
|
|
207
|
+
}
|
|
208
|
+
return n;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The octets one literal string takes on the wire: its length prefix and the
|
|
212
|
+
// string itself, Huffman-coded when that is shorter.
|
|
213
|
+
function strLen(s, huffBits) {
|
|
214
|
+
const hlen = (huffBits + 7) >> 3;
|
|
215
|
+
const len = hlen < s.length ? hlen : s.length;
|
|
216
|
+
return intLen(len, 7) + len;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
const POW2 = new Float64Array(53);
|
|
221
|
+
for (let i = 0; i < 53; i++) {
|
|
222
|
+
POW2[i] = 2 ** i;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Bit accumulation must not use 32-bit operators: after adding a 30-bit code the
|
|
226
|
+
// buffer can hold up to 37 bits. Doubles are exact below 2^53, so multiply/divide.
|
|
227
|
+
function huffEncode(src, start, end, dst, off) {
|
|
228
|
+
let acc = 0;
|
|
229
|
+
let nbits = 0;
|
|
230
|
+
for (let i = start; i < end; i++) {
|
|
231
|
+
const b = HUFF_BITS[src[i]];
|
|
232
|
+
acc = acc * POW2[b] + HUFF_CODES[src[i]];
|
|
233
|
+
nbits += b;
|
|
234
|
+
while (nbits >= 8) {
|
|
235
|
+
nbits -= 8;
|
|
236
|
+
const byte = Math.floor(acc / POW2[nbits]);
|
|
237
|
+
dst[off++] = byte;
|
|
238
|
+
acc -= byte * POW2[nbits];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// EOS is the all-ones code, so padding the last byte with ones *is* EOS
|
|
242
|
+
if (nbits > 0) {
|
|
243
|
+
const pad = POW2[8 - nbits] - 1;
|
|
244
|
+
dst[off++] = acc * POW2[8 - nbits] + pad;
|
|
245
|
+
}
|
|
246
|
+
return off;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The same encoder for a string: latin1 truncates every code unit to its low byte,
|
|
250
|
+
// so the byte of a character is its code unit & 0xff -- the byte the buffer version
|
|
251
|
+
// above would have been handed. Coding straight from the string saves the copy into
|
|
252
|
+
// a scratch buffer and the view of it that the copy needed (OPTIMIZATION.md item 12).
|
|
253
|
+
function huffEncodeStr(s, dst, off) {
|
|
254
|
+
let acc = 0;
|
|
255
|
+
let nbits = 0;
|
|
256
|
+
for (let i = 0; i < s.length; i++) {
|
|
257
|
+
const sym = s.charCodeAt(i) & 0xff;
|
|
258
|
+
const b = HUFF_BITS[sym];
|
|
259
|
+
acc = acc * POW2[b] + HUFF_CODES[sym];
|
|
260
|
+
nbits += b;
|
|
261
|
+
while (nbits >= 8) {
|
|
262
|
+
nbits -= 8;
|
|
263
|
+
const byte = Math.floor(acc / POW2[nbits]);
|
|
264
|
+
dst[off++] = byte;
|
|
265
|
+
acc -= byte * POW2[nbits];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// EOS is the all-ones code, so padding the last byte with ones *is* EOS
|
|
269
|
+
if (nbits > 0) {
|
|
270
|
+
const pad = POW2[8 - nbits] - 1;
|
|
271
|
+
dst[off++] = acc * POW2[8 - nbits] + pad;
|
|
272
|
+
}
|
|
273
|
+
return off;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The octets of a string into a buffer at `off`: latin1, one byte per code unit.
|
|
277
|
+
// Short strings go straight from charCodeAt, long through Buffer#write('latin1')
|
|
278
|
+
// -- the encode-side mirror of the latin1() helper below. Threshold and
|
|
279
|
+
// measurement: OPTIMIZATION.md item 35.
|
|
280
|
+
function latin1Into(dst, off, s) {
|
|
281
|
+
const len = s.length;
|
|
282
|
+
if (len <= RAW_WRITE_MAX) {
|
|
283
|
+
for (let i = 0; i < len; i++) {
|
|
284
|
+
dst[off + i] = s.charCodeAt(i);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
dst.write(s, off, 'latin1');
|
|
288
|
+
}
|
|
289
|
+
return off + len;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Returns the decoded byte count, or -1 if the input is not a valid sequence of
|
|
293
|
+
// complete codes (an explicit EOS, or a tail that is neither empty nor all ones).
|
|
294
|
+
function huffDecode(src, start, end, dst) {
|
|
295
|
+
let node = 0;
|
|
296
|
+
let out = 0;
|
|
297
|
+
let tail = 0;
|
|
298
|
+
for (let i = start; i < end; i++) {
|
|
299
|
+
const byte = src[i];
|
|
300
|
+
for (let k = 7; k >= 0; k--) {
|
|
301
|
+
node = ((byte >>> k) & 1) ? TR_RIGHT[node] : TR_LEFT[node];
|
|
302
|
+
const sym = TR_LEAF[node];
|
|
303
|
+
if (sym !== -1) {
|
|
304
|
+
if (sym === EOS) {
|
|
305
|
+
return -1;
|
|
306
|
+
}
|
|
307
|
+
dst[out++] = sym;
|
|
308
|
+
node = 0;
|
|
309
|
+
tail = 0;
|
|
310
|
+
} else {
|
|
311
|
+
tail++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// the unwalked tail is the last `tail` bits of the stream; a valid padding is
|
|
316
|
+
// a prefix of EOS, i.e. at most 7 bits, all of them ones
|
|
317
|
+
if (tail > 7) {
|
|
318
|
+
return -1;
|
|
319
|
+
}
|
|
320
|
+
if (tail > 0 && (src[end - 1] & ((1 << tail) - 1)) !== (1 << tail) - 1) {
|
|
321
|
+
return -1;
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---- decoder ------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
// The stand-in for a view past the end of the range: reading from it is a bug
|
|
329
|
+
// the range check above catches first.
|
|
330
|
+
const EMPTY = Buffer.alloc(0);
|
|
331
|
+
|
|
332
|
+
// A latin1 string out of a buffer: the octets become code points, which is what
|
|
333
|
+
// latin1 decoding is -- the library keeps a field value's octets one to one
|
|
334
|
+
// (RFC 9113 8.2). Switch, chunked default and the measurement that shaped
|
|
335
|
+
// both: OPTIMIZATION.md item 13.
|
|
336
|
+
function latin1(buf, start, end) {
|
|
337
|
+
const len = end - start;
|
|
338
|
+
switch (len) {
|
|
339
|
+
case 0: return '';
|
|
340
|
+
case 1: return String.fromCharCode(buf[start]);
|
|
341
|
+
case 2: return String.fromCharCode(buf[start], buf[start + 1]);
|
|
342
|
+
case 3: return String.fromCharCode(buf[start], buf[start + 1], buf[start + 2]);
|
|
343
|
+
case 4: return String.fromCharCode(buf[start], buf[start + 1], buf[start + 2], buf[start + 3]);
|
|
344
|
+
default: {
|
|
345
|
+
// the rest is at most 8 octets, so the recursion is at most two calls deep
|
|
346
|
+
if (len <= 12) {
|
|
347
|
+
return String.fromCharCode(buf[start], buf[start + 1], buf[start + 2], buf[start + 3])
|
|
348
|
+
+ latin1(buf, start + 4, end);
|
|
349
|
+
}
|
|
350
|
+
return buf.toString('latin1', start, end);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// A cursor over the octets of a header block that arrived in pieces.
|
|
356
|
+
//
|
|
357
|
+
// The block is a list of views of the socket chunks it came in, and the octets
|
|
358
|
+
// of a field line -- its prefix, the length of a literal, the literal itself --
|
|
359
|
+
// may straddle the boundary between two of them. The cursor walks the views in
|
|
360
|
+
// order and is the piece-aware twin of the module-level slots the contiguous
|
|
361
|
+
// decoder reads into (RI/RP): same rules, same order, same table updates, only
|
|
362
|
+
// the place the octets come from is different.
|
|
363
|
+
//
|
|
364
|
+
// test/hpack.test.js pins the two decoders against each other over the same
|
|
365
|
+
// blocks, so the twin cannot drift away from the original.
|
|
366
|
+
class PieceCursor {
|
|
367
|
+
// `pieces` are the views in the order they arrived; `start` and `end` are
|
|
368
|
+
// offsets into their concatenation.
|
|
369
|
+
constructor(pieces, start, end) {
|
|
370
|
+
this.pieces = pieces;
|
|
371
|
+
this.i = -1; // the view the cursor is in
|
|
372
|
+
this.o = 0; // the offset inside it
|
|
373
|
+
this.left = end - start; // the octets left in the range
|
|
374
|
+
this.value = 0; // the last integer read
|
|
375
|
+
this.cur = EMPTY;
|
|
376
|
+
this.pieceEnd = 0;
|
|
377
|
+
this.next();
|
|
378
|
+
let skip = start;
|
|
379
|
+
while (skip > 0) {
|
|
380
|
+
const avail = this.pieceEnd - this.o;
|
|
381
|
+
if (skip < avail) {
|
|
382
|
+
this.o += skip; skip = 0;
|
|
383
|
+
} else {
|
|
384
|
+
skip -= avail; this.next();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The next view with octets in it. An empty one is skipped: a frame whose
|
|
390
|
+
// payload starts exactly at the end of a read arrives as an empty view and
|
|
391
|
+
// the octets that follow, and the cursor may not sit on an empty one.
|
|
392
|
+
next() {
|
|
393
|
+
this.i++;
|
|
394
|
+
this.o = 0;
|
|
395
|
+
while (this.i < this.pieces.length && this.pieces[this.i].length === 0) {
|
|
396
|
+
this.i++;
|
|
397
|
+
}
|
|
398
|
+
if (this.i >= this.pieces.length) {
|
|
399
|
+
// past the last view: the range is over, and nothing may be read from it
|
|
400
|
+
this.cur = EMPTY;
|
|
401
|
+
this.pieceEnd = 0;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
this.cur = this.pieces[this.i];
|
|
405
|
+
this.pieceEnd = this.cur.length;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// The octet at the cursor, without moving it.
|
|
409
|
+
peek() {
|
|
410
|
+
return this.cur[this.o];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The octet at the cursor, moving it on.
|
|
414
|
+
byte() {
|
|
415
|
+
const b = this.cur[this.o++];
|
|
416
|
+
this.left--;
|
|
417
|
+
if (this.o === this.pieceEnd) {
|
|
418
|
+
this.next();
|
|
419
|
+
}
|
|
420
|
+
return b;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// RFC 7541 5.1: the first octet carries `bits` bits of the value, and each
|
|
424
|
+
// continuation octet carries seven bits more, the first of them weighing 1.
|
|
425
|
+
readInt(bits) {
|
|
426
|
+
const mask = (1 << bits) - 1;
|
|
427
|
+
if (this.left <= 0) {
|
|
428
|
+
throw new HPackError('hpack: truncated integer');
|
|
429
|
+
}
|
|
430
|
+
let v = this.byte() & mask;
|
|
431
|
+
if (v < mask) {
|
|
432
|
+
this.value = v; return;
|
|
433
|
+
}
|
|
434
|
+
let mul = 1;
|
|
435
|
+
while (this.left > 0) {
|
|
436
|
+
const b = this.byte();
|
|
437
|
+
v += (b & 0x7f) * mul;
|
|
438
|
+
if (v > 0x7fffffff) {
|
|
439
|
+
throw new HPackError('hpack: integer too large');
|
|
440
|
+
}
|
|
441
|
+
mul *= 128;
|
|
442
|
+
if ((b & 0x80) === 0) {
|
|
443
|
+
this.value = v; return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
throw new HPackError('hpack: truncated integer');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// The next `len` octets as a latin1 string. Inside one view: the native
|
|
450
|
+
// conversion and nothing in between. Across several: one conversion per view,
|
|
451
|
+
// which is what a string that a socket read split costs.
|
|
452
|
+
str(len) {
|
|
453
|
+
if (this.o + len <= this.pieceEnd) {
|
|
454
|
+
const whole = latin1(this.cur, this.o, this.o + len);
|
|
455
|
+
this.o += len;
|
|
456
|
+
this.left -= len;
|
|
457
|
+
if (this.o === this.pieceEnd) {
|
|
458
|
+
this.next();
|
|
459
|
+
}
|
|
460
|
+
return whole;
|
|
461
|
+
}
|
|
462
|
+
let s = '';
|
|
463
|
+
let left = len;
|
|
464
|
+
while (left > 0) {
|
|
465
|
+
const avail = this.pieceEnd - this.o;
|
|
466
|
+
if (avail === 0) {
|
|
467
|
+
throw new HPackError('hpack: truncated literal');
|
|
468
|
+
}
|
|
469
|
+
const take = avail < left ? avail : left;
|
|
470
|
+
s += latin1(this.cur, this.o, this.o + take);
|
|
471
|
+
this.o += take;
|
|
472
|
+
this.left -= take;
|
|
473
|
+
left -= take;
|
|
474
|
+
if (this.o === this.pieceEnd) {
|
|
475
|
+
this.next();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return s;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The Huffman decoder over a run of octets that arrived in pieces: the same
|
|
483
|
+
// walk as huffDecode(), with the octets coming from the cursor.
|
|
484
|
+
function huffDecodePieces(cur, len, dst) {
|
|
485
|
+
let node = 0;
|
|
486
|
+
let out = 0;
|
|
487
|
+
let tail = 0;
|
|
488
|
+
let last = 0;
|
|
489
|
+
for (let i = 0; i < len; i++) {
|
|
490
|
+
const byte = cur.byte();
|
|
491
|
+
last = byte;
|
|
492
|
+
for (let k = 7; k >= 0; k--) {
|
|
493
|
+
node = ((byte >>> k) & 1) ? TR_RIGHT[node] : TR_LEFT[node];
|
|
494
|
+
const sym = TR_LEAF[node];
|
|
495
|
+
if (sym !== -1) {
|
|
496
|
+
if (sym === EOS) {
|
|
497
|
+
return -1;
|
|
498
|
+
}
|
|
499
|
+
dst[out++] = sym;
|
|
500
|
+
node = 0;
|
|
501
|
+
tail = 0;
|
|
502
|
+
} else {
|
|
503
|
+
tail++;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// the unwalked tail is the last `tail` bits of the stream; a valid padding is
|
|
508
|
+
// a prefix of EOS, i.e. at most 7 bits, all of them ones
|
|
509
|
+
if (tail > 7) {
|
|
510
|
+
return -1;
|
|
511
|
+
}
|
|
512
|
+
if (tail > 0 && (last & ((1 << tail) - 1)) !== (1 << tail) - 1) {
|
|
513
|
+
return -1;
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
class Decoder {
|
|
519
|
+
// `capacity` = what we advertised in our SETTINGS_HEADER_TABLE_SIZE
|
|
520
|
+
constructor(capacity = 4096) {
|
|
521
|
+
this.capacity = capacity;
|
|
522
|
+
this.maxSize = capacity;
|
|
523
|
+
this.size = 0;
|
|
524
|
+
this.dyn = []; // oldest first; entries below `base` are evicted
|
|
525
|
+
this.base = 0;
|
|
526
|
+
this.epoch = 0; // bumped on every table change (see §4.4, 4.3)
|
|
527
|
+
this.names = new Map(); // interned field names
|
|
528
|
+
this.scratch = Buffer.allocUnsafe(4096);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// HPACK index (1-based, newest dynamic entry first) -> [name, value]
|
|
532
|
+
at(index) {
|
|
533
|
+
if (index >= 1 && index <= STATIC_LEN) {
|
|
534
|
+
return STATIC[index - 1];
|
|
535
|
+
}
|
|
536
|
+
const j = index - STATIC_LEN - 1;
|
|
537
|
+
if (j < 0 || j >= this.dyn.length - this.base) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
return this.dyn[this.dyn.length - 1 - j];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
insert(name, value) {
|
|
544
|
+
this.epoch++;
|
|
545
|
+
const sz = name.length + value.length + 32;
|
|
546
|
+
if (sz > this.maxSize) { // RFC 7541 §4.4: table ends up empty
|
|
547
|
+
this.dyn.length = this.base = 0;
|
|
548
|
+
this.size = 0;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.dyn.push([name, value]);
|
|
552
|
+
this.size += sz;
|
|
553
|
+
while (this.size > this.maxSize && this.dyn.length > this.base + 1) {
|
|
554
|
+
const old = this.dyn[this.base++];
|
|
555
|
+
this.size -= old[0].length + old[1].length + 32;
|
|
556
|
+
}
|
|
557
|
+
if (this.base > 64) { // amortised compaction
|
|
558
|
+
this.dyn.copyWithin(0, this.base);
|
|
559
|
+
this.dyn.length -= this.base;
|
|
560
|
+
this.base = 0;
|
|
561
|
+
this.size = 0;
|
|
562
|
+
for (const e of this.dyn) {
|
|
563
|
+
this.size += e[0].length + e[1].length + 32;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
setMaxSize(n) {
|
|
569
|
+
if (n > this.capacity) {
|
|
570
|
+
throw new HPackError('hpack: table size update exceeds advertised capacity');
|
|
571
|
+
}
|
|
572
|
+
if (this.maxSize !== n) {
|
|
573
|
+
this.epoch++;
|
|
574
|
+
}
|
|
575
|
+
this.maxSize = n;
|
|
576
|
+
while (this.size > n && this.dyn.length > this.base) {
|
|
577
|
+
const old = this.dyn[this.base++];
|
|
578
|
+
this.size -= old[0].length + old[1].length + 32;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Refills the reusable flat array `out` with name, value, name, value, ...
|
|
583
|
+
// and returns the number of field lines.
|
|
584
|
+
decode(buf, start, end, out) {
|
|
585
|
+
out.length = 0;
|
|
586
|
+
let p = start;
|
|
587
|
+
while (p < end) {
|
|
588
|
+
const b = buf[p];
|
|
589
|
+
if (b & 0x80) { // 1xxxxxxx indexed
|
|
590
|
+
readInt(buf, p, 7, end);
|
|
591
|
+
const e = this.at(RI);
|
|
592
|
+
if (e === undefined) {
|
|
593
|
+
throw new HPackError(`hpack: invalid index ${RI}`);
|
|
594
|
+
}
|
|
595
|
+
out.push(e[0], e[1]);
|
|
596
|
+
p = RP;
|
|
597
|
+
} else if ((b & 0xc0) === 0x40) { // 01xxxxxx incremental
|
|
598
|
+
p = this.literal(buf, p, end, out, 6);
|
|
599
|
+
this.insert(out[out.length - 2], out[out.length - 1]);
|
|
600
|
+
} else if ((b & 0xe0) === 0x20) { // 001xxxxx size update
|
|
601
|
+
// RFC 7541 6.3: a size update may only appear at the beginning of a header block --
|
|
602
|
+
// before its first field line, and `out.length === 0` is exactly that. Measured on node
|
|
603
|
+
// (h2spec hpack/4.2), one that follows a field line is GOAWAY(COMPRESSION_ERROR) whether
|
|
604
|
+
// or not it changes anything: the indices of the lines already decoded name a table that
|
|
605
|
+
// the update says was different, so a second decoder that reads the block in the order
|
|
606
|
+
// the octets arrive would rebuild a different context from ours.
|
|
607
|
+
if (out.length !== 0) {
|
|
608
|
+
throw new HPackError('hpack: table size update after a field line');
|
|
609
|
+
}
|
|
610
|
+
readInt(buf, p, 5, end);
|
|
611
|
+
this.setMaxSize(RI);
|
|
612
|
+
p = RP;
|
|
613
|
+
} else { // 0000/0001 literal
|
|
614
|
+
p = this.literal(buf, p, end, out, 4);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return out.length >> 1;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
literal(buf, p, end, out, bits) {
|
|
621
|
+
readInt(buf, p, bits, end);
|
|
622
|
+
let name;
|
|
623
|
+
if (RI === 0) {
|
|
624
|
+
const s = this.str(buf, RP, end);
|
|
625
|
+
name = this.intern(s.value);
|
|
626
|
+
p = s.p;
|
|
627
|
+
} else {
|
|
628
|
+
const e = this.at(RI);
|
|
629
|
+
if (e === undefined) {
|
|
630
|
+
throw new HPackError(`hpack: invalid name index ${RI}`);
|
|
631
|
+
}
|
|
632
|
+
name = e[0];
|
|
633
|
+
p = RP;
|
|
634
|
+
}
|
|
635
|
+
const v = this.str(buf, p, end);
|
|
636
|
+
out.push(name, v.value);
|
|
637
|
+
return v.p;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
str(buf, p, end) {
|
|
641
|
+
if (p >= end) {
|
|
642
|
+
throw new HPackError('hpack: truncated string');
|
|
643
|
+
}
|
|
644
|
+
const huff = (buf[p] & 0x80) !== 0;
|
|
645
|
+
readInt(buf, p, 7, end);
|
|
646
|
+
const len = RI;
|
|
647
|
+
p = RP;
|
|
648
|
+
if (len > MAX_LITERAL) {
|
|
649
|
+
throw new HPackError('hpack: literal too long');
|
|
650
|
+
}
|
|
651
|
+
if (p + len > end) {
|
|
652
|
+
throw new HPackError('hpack: truncated literal');
|
|
653
|
+
}
|
|
654
|
+
if (!huff) {
|
|
655
|
+
return { value: latin1(buf, p, p + len), p: p + len };
|
|
656
|
+
}
|
|
657
|
+
// the shortest code is 5 bits, so `len` encoded bytes decode to <= len*8/5
|
|
658
|
+
const need = (len * 8) / 5 + 8 | 0;
|
|
659
|
+
if (this.scratch.length < need) {
|
|
660
|
+
this.scratch = Buffer.allocUnsafe(need);
|
|
661
|
+
}
|
|
662
|
+
const n = huffDecode(buf, p, p + len, this.scratch);
|
|
663
|
+
if (n < 0) {
|
|
664
|
+
throw new HPackError('hpack: invalid huffman encoding');
|
|
665
|
+
}
|
|
666
|
+
return { value: latin1(this.scratch, 0, n), p: p + len };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// The same decoder over a block that arrived in pieces: the octets are read
|
|
670
|
+
// through a cursor that walks the views the block arrived in, instead of out
|
|
671
|
+
// of one buffer. The rules, the order and the table updates are the ones
|
|
672
|
+
// decode() applies -- this is the same decoder, not a second set of rules.
|
|
673
|
+
//
|
|
674
|
+
// Only a block that a socket read split takes this path. A block that arrived
|
|
675
|
+
// whole is decoded out of the chunk it came in, with no cursor and no copy.
|
|
676
|
+
decodePieces(pieces, start, end, out) {
|
|
677
|
+
out.length = 0;
|
|
678
|
+
const cur = new PieceCursor(pieces, start, end);
|
|
679
|
+
while (cur.left > 0) {
|
|
680
|
+
const b = cur.peek();
|
|
681
|
+
if (b & 0x80) { // 1xxxxxxx indexed
|
|
682
|
+
cur.readInt(7);
|
|
683
|
+
const e = this.at(cur.value);
|
|
684
|
+
if (e === undefined) {
|
|
685
|
+
throw new HPackError(`hpack: invalid index ${cur.value}`);
|
|
686
|
+
}
|
|
687
|
+
out.push(e[0], e[1]);
|
|
688
|
+
} else if ((b & 0xc0) === 0x40) { // 01xxxxxx incremental
|
|
689
|
+
this.literalPieces(cur, out, 6);
|
|
690
|
+
this.insert(out[out.length - 2], out[out.length - 1]);
|
|
691
|
+
} else if ((b & 0xe0) === 0x20) { // 001xxxxx size update
|
|
692
|
+
// RFC 7541 6.3: a size update may only appear at the beginning of a header
|
|
693
|
+
// block -- before its first field line. See decode() for the measurement.
|
|
694
|
+
if (out.length !== 0) {
|
|
695
|
+
throw new HPackError('hpack: table size update after a field line');
|
|
696
|
+
}
|
|
697
|
+
cur.readInt(5);
|
|
698
|
+
this.setMaxSize(cur.value);
|
|
699
|
+
} else { // 0000/0001 literal
|
|
700
|
+
this.literalPieces(cur, out, 4);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out.length >> 1;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
literalPieces(cur, out, bits) {
|
|
707
|
+
cur.readInt(bits);
|
|
708
|
+
let name;
|
|
709
|
+
if (cur.value === 0) {
|
|
710
|
+
name = this.intern(this.strPieces(cur));
|
|
711
|
+
} else {
|
|
712
|
+
const e = this.at(cur.value);
|
|
713
|
+
if (e === undefined) {
|
|
714
|
+
throw new HPackError(`hpack: invalid name index ${cur.value}`);
|
|
715
|
+
}
|
|
716
|
+
name = e[0];
|
|
717
|
+
}
|
|
718
|
+
const v = this.strPieces(cur);
|
|
719
|
+
out.push(name, v);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// A literal string out of the views: the length prefix, then the octets. The
|
|
723
|
+
// prefix's top bit says whether the octets are Huffman-coded.
|
|
724
|
+
strPieces(cur) {
|
|
725
|
+
const huff = (cur.peek() & 0x80) !== 0;
|
|
726
|
+
cur.readInt(7);
|
|
727
|
+
const len = cur.value;
|
|
728
|
+
if (len > MAX_LITERAL) {
|
|
729
|
+
throw new HPackError('hpack: literal too long');
|
|
730
|
+
}
|
|
731
|
+
if (cur.left < len) {
|
|
732
|
+
throw new HPackError('hpack: truncated literal');
|
|
733
|
+
}
|
|
734
|
+
if (!huff) {
|
|
735
|
+
return cur.str(len);
|
|
736
|
+
}
|
|
737
|
+
// the shortest code is 5 bits, so `len` encoded octets decode to <= len*8/5
|
|
738
|
+
const need = (len * 8) / 5 + 8 | 0;
|
|
739
|
+
if (this.scratch.length < need) {
|
|
740
|
+
this.scratch = Buffer.allocUnsafe(need);
|
|
741
|
+
}
|
|
742
|
+
const n = huffDecodePieces(cur, len, this.scratch);
|
|
743
|
+
if (n < 0) {
|
|
744
|
+
throw new HPackError('hpack: invalid huffman encoding');
|
|
745
|
+
}
|
|
746
|
+
return latin1(this.scratch, 0, n);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
intern(name) {
|
|
750
|
+
const hit = this.names.get(name);
|
|
751
|
+
if (hit !== undefined) {
|
|
752
|
+
return hit;
|
|
753
|
+
}
|
|
754
|
+
if (this.names.size > 4096) {
|
|
755
|
+
this.names.clear();
|
|
756
|
+
}
|
|
757
|
+
this.names.set(name, name);
|
|
758
|
+
return name;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ---- encoder ------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
// Names whose values are not to be remembered in a table on either side: the five that
|
|
765
|
+
// RFC 7541 7.1.3 names and its A.2 example list carries -- credentials and session
|
|
766
|
+
// tokens. The application's own sensitiveHeaders path (the neverIndex flag, 0x10) is the
|
|
767
|
+
// per-call mechanism; this is the default for the headers an application did not mark.
|
|
768
|
+
//
|
|
769
|
+
// nghttp2 refuses to index a longer list (lib/nghttp2_hd.c v1.70.0,
|
|
770
|
+
// hd_deflate_decide_indexing(): :path, age, content-length, etag, if-modified-since,
|
|
771
|
+
// if-none-match, location, set-cookie) on the theory that a per-response value is not
|
|
772
|
+
// worth a table entry. That list was tried here, against this encoder's second-sighting
|
|
773
|
+
// rule, and it lost on both axes: an ETag is per-*resource*, so it repeats, and
|
|
774
|
+
// indexing it is what makes a cached response cheap (OPTIMIZATION.md item 27). The
|
|
775
|
+
// churn the name list protects against is already handled by REINSERT_MAX below, which
|
|
776
|
+
// reacts to the eviction pattern instead of guessing from the name. Only the size half
|
|
777
|
+
// of that rule is kept (see compile()).
|
|
778
|
+
const NEVER_INSERT = new Set(['set-cookie', 'www-authenticate', 'proxy-authenticate',
|
|
779
|
+
'authorization', 'cookie']);
|
|
780
|
+
|
|
781
|
+
// How often a table entry has to come back after being evicted before this session
|
|
782
|
+
// gives up on its dynamic table. See insert().
|
|
783
|
+
const REINSERT_MAX = 8;
|
|
784
|
+
|
|
785
|
+
// Raw literal strings at or below this length are written straight from charCodeAt
|
|
786
|
+
// instead of through Buffer#write('latin1'); see latin1Into() and OPTIMIZATION.md
|
|
787
|
+
// item 35 for the measurement that set the threshold.
|
|
788
|
+
const RAW_WRITE_MAX = 16;
|
|
789
|
+
|
|
790
|
+
// How many distinct values one name may have before the encoder stops remembering
|
|
791
|
+
// them. The "seen exactly once" set is what makes the second-sighting rule work,
|
|
792
|
+
// and the set is not free: the value is a fresh string, so every check hashes it
|
|
793
|
+
// and every insert grows a Set that reaches 8192 entries per session. A name whose
|
|
794
|
+
// values never repeat -- the request id a server echoes in every response -- pays
|
|
795
|
+
// that for every response and gets nothing for it, because a pair that is
|
|
796
|
+
// remembered here is a pair that will be inserted on its second sighting, and that
|
|
797
|
+
// second sighting never comes.
|
|
798
|
+
//
|
|
799
|
+
// Past this many distinct values the name is treated as one whose values are
|
|
800
|
+
// unique to the response: the check is skipped and the line stays a literal.
|
|
801
|
+
// A name whose values do repeat keeps its entries -- the set holds the values seen
|
|
802
|
+
// exactly once, and a value that comes back is removed from it -- so the bound only
|
|
803
|
+
// bites on a name that has already produced 64 values nobody asked for twice
|
|
804
|
+
// (the cost of the check: OPTIMIZATION.md item 11).
|
|
805
|
+
const ONCE_PER_NAME = 64;
|
|
806
|
+
|
|
807
|
+
// The encoded form of a field name: the length prefix and the octets.
|
|
808
|
+
//
|
|
809
|
+
// A name is written in full whenever it is in neither the static nor the dynamic
|
|
810
|
+
// table, and a response whose header block cannot be cached as a whole pays that
|
|
811
|
+
// for every response -- a server that echoes a request id writes the same name
|
|
812
|
+
// with a new value every time. Huffman-coding the name again for every one of
|
|
813
|
+
// those values is the waste this cache removes (OPTIMIZATION.md item 11).
|
|
814
|
+
//
|
|
815
|
+
// The bytes depend on the name alone -- the length prefix and the octets, not the
|
|
816
|
+
// representation prefix in front of them -- so the cache is process-wide and not
|
|
817
|
+
// per session. Bounded like VALID_NAMES in lib/headers.js: the names come from
|
|
818
|
+
// the application, and a name that is new to a full cache is encoded as before.
|
|
819
|
+
const NAME_BYTES = new Map();
|
|
820
|
+
const NAME_BYTES_MAX = 1024;
|
|
821
|
+
|
|
822
|
+
// The name's own encoding, the same octets literalStrInto() would write.
|
|
823
|
+
function nameBytes(name) {
|
|
824
|
+
let b = NAME_BYTES.get(name);
|
|
825
|
+
if (b === undefined) {
|
|
826
|
+
const huffBits = huffBitsOf(name);
|
|
827
|
+
const hlen = (huffBits + 7) >> 3;
|
|
828
|
+
const huff = hlen < name.length;
|
|
829
|
+
const len = huff ? hlen : name.length;
|
|
830
|
+
b = Buffer.allocUnsafe(intLen(len, 7) + len);
|
|
831
|
+
const off = writeInt(b, 0, len, 7, huff ? 0x80 : 0x00);
|
|
832
|
+
if (huff) {
|
|
833
|
+
huffEncodeStr(name, b, off);
|
|
834
|
+
} else {
|
|
835
|
+
b.write(name, off, 'latin1');
|
|
836
|
+
}
|
|
837
|
+
if (NAME_BYTES.size < NAME_BYTES_MAX) {
|
|
838
|
+
NAME_BYTES.set(name, b);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return b;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// The size of the name cache, for the test that pins the bound.
|
|
845
|
+
function nameBytesSize() {
|
|
846
|
+
return NAME_BYTES.size;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// The shape of a literal, in module-level slots: the name it uses (an index, or 0
|
|
850
|
+
// for the full name), the Huffman bits of its two strings, and the octets the whole
|
|
851
|
+
// line takes. Asking for it costs no allocation, which is why it is not an object.
|
|
852
|
+
let L_BITS = 0;
|
|
853
|
+
let L_NAME_IDX = 0;
|
|
854
|
+
let L_NAME_BYTES = null;
|
|
855
|
+
let L_VALUE_BITS = 0;
|
|
856
|
+
let L_NEED = 0;
|
|
857
|
+
|
|
858
|
+
class Encoder {
|
|
859
|
+
// `peerSize` is the peer's SETTINGS_HEADER_TABLE_SIZE -- the size it assumes the table
|
|
860
|
+
// we build for it has -- and `cap` is the ceiling we are willing to spend on it. The
|
|
861
|
+
// two are normally the same number, and when they are not we have to announce ours, or
|
|
862
|
+
// the peer would evict at a different point than we do and the tables would drift.
|
|
863
|
+
constructor(peerSize = 4096, cap = peerSize) {
|
|
864
|
+
this.cap = cap;
|
|
865
|
+
this.peerSize = peerSize;
|
|
866
|
+
this.maxSize = Math.min(peerSize, cap);
|
|
867
|
+
this.size = 0;
|
|
868
|
+
this.dyn = []; // oldest first; everything below `base` is evicted
|
|
869
|
+
this.base = 0;
|
|
870
|
+
this.epoch = 0; // bumped on every change the peer's table mirrors
|
|
871
|
+
this.pendingSize = this.maxSize === peerSize ? null : this.maxSize;
|
|
872
|
+
// The encoded bytes of a field line, in two levels: the name, then the value.
|
|
873
|
+
// The never-indexed form is a map of its own, because the same pair can be sent
|
|
874
|
+
// both ways and the two forms are different bytes.
|
|
875
|
+
this.cache = new Map(); // name -> Map<value, Buffer>
|
|
876
|
+
this.cacheNever = new Map(); // name -> Map<value, Buffer>
|
|
877
|
+
this.cacheCount = 0; // field lines in both maps
|
|
878
|
+
// The pairs seen exactly once: not worth an insertion yet. And the pairs whose
|
|
879
|
+
// entry left the table, oldest signal first. Both in two levels, for the same
|
|
880
|
+
// reason as the byte cache above.
|
|
881
|
+
this.once = new Map(); // name -> Set<value>
|
|
882
|
+
this.onceCount = 0;
|
|
883
|
+
this.evicted = new Map(); // name -> Set<value>
|
|
884
|
+
this.evictedCount = 0;
|
|
885
|
+
this.reinserts = 0; // of them, how often we had to put one back
|
|
886
|
+
this.noInsert = false; // the working set does not fit; stop inserting
|
|
887
|
+
this.unsettled = false; // set by a line whose form will change next time
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Newest dynamic index holding exactly this pair, 0 when absent. Only reached on a
|
|
891
|
+
// byte-cache miss, so the scan of a table of at most ~100 entries is cold-path work.
|
|
892
|
+
findFull(name, value) {
|
|
893
|
+
for (let k = this.dyn.length - 1; k >= this.base; k--) {
|
|
894
|
+
const e = this.dyn[k];
|
|
895
|
+
if (e[0] === name && e[1] === value) {
|
|
896
|
+
return STATIC_LEN + this.dyn.length - k;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return 0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
findName(name) {
|
|
903
|
+
for (let k = this.dyn.length - 1; k >= this.base; k--) {
|
|
904
|
+
if (this.dyn[k][0] === name) {
|
|
905
|
+
return STATIC_LEN + this.dyn.length - k;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return 0;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Adds an entry and evicts to fit. Every cached byte sequence may name an index that
|
|
912
|
+
// just moved, so the cache cannot survive the change -- which is why insertion is
|
|
913
|
+
// reserved for values that repeat.
|
|
914
|
+
insert(name, value) {
|
|
915
|
+
const sz = name.length + value.length + 32;
|
|
916
|
+
let dropped = 0;
|
|
917
|
+
if (sz > this.maxSize) { // RFC 7541 4.4: the table ends up empty
|
|
918
|
+
this.rememberEvicted(this.dyn, this.base, this.dyn.length);
|
|
919
|
+
this.dyn.length = this.base = 0;
|
|
920
|
+
this.size = 0;
|
|
921
|
+
} else {
|
|
922
|
+
this.dyn.push([name, value]);
|
|
923
|
+
this.size += sz;
|
|
924
|
+
const from = this.base;
|
|
925
|
+
while (this.size > this.maxSize && this.dyn.length > this.base + 1) {
|
|
926
|
+
const old = this.dyn[this.base++];
|
|
927
|
+
this.size -= old[0].length + old[1].length + 32;
|
|
928
|
+
dropped++;
|
|
929
|
+
}
|
|
930
|
+
if (dropped > 0) {
|
|
931
|
+
this.rememberEvicted(this.dyn, from, this.base);
|
|
932
|
+
}
|
|
933
|
+
if (this.base > 64) { // amortised compaction
|
|
934
|
+
this.dyn.copyWithin(0, this.base);
|
|
935
|
+
this.dyn.length -= this.base;
|
|
936
|
+
this.base = 0;
|
|
937
|
+
this.size = 0;
|
|
938
|
+
for (const e of this.dyn) {
|
|
939
|
+
this.size += e[0].length + e[1].length + 32;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
this.touch();
|
|
944
|
+
// The signal that matters is not the number of evictions but whether an evicted
|
|
945
|
+
// entry was still wanted: if the same pair comes back and has to be inserted again,
|
|
946
|
+
// the working set is bigger than the table. A workload that fits never re-inserts
|
|
947
|
+
// anything, so this cannot fire during its warm-up.
|
|
948
|
+
if (this.evictedDelete(name, value) && ++this.reinserts >= REINSERT_MAX) {
|
|
949
|
+
// Measured (OPTIMIZATION.md item 11): a working set that
|
|
950
|
+
// keeps overflowing the table spends a multiple of the no-table cost on every
|
|
951
|
+
// block, because each eviction invalidates both byte caches -- and inserting stops
|
|
952
|
+
// returns it within a few hundred responses. Nothing has to be announced for
|
|
953
|
+
// that: the table keeps what is in it, stops moving, and so do the caches --
|
|
954
|
+
// the peer's decoder sees exactly the same indices, because we never touch
|
|
955
|
+
// them again.
|
|
956
|
+
this.noInsert = true;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
rememberEvicted(dyn, from, to) {
|
|
961
|
+
if (this.evictedCount >= CACHE_LIMIT) {
|
|
962
|
+
this.evicted.clear();
|
|
963
|
+
this.evictedCount = 0;
|
|
964
|
+
}
|
|
965
|
+
for (let k = from; k < to; k++) {
|
|
966
|
+
const e = dyn[k];
|
|
967
|
+
let inner = this.evicted.get(e[0]);
|
|
968
|
+
if (inner === undefined) {
|
|
969
|
+
inner = new Set();
|
|
970
|
+
this.evicted.set(e[0], inner);
|
|
971
|
+
}
|
|
972
|
+
if (!inner.has(e[1])) {
|
|
973
|
+
inner.add(e[1]);
|
|
974
|
+
this.evictedCount++;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Whether this pair was evicted before, and forgets it: it is back, so the entry
|
|
980
|
+
// it had is wanted again.
|
|
981
|
+
evictedDelete(name, value) {
|
|
982
|
+
const inner = this.evicted.get(name);
|
|
983
|
+
if (inner === undefined) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
const had = inner.delete(value);
|
|
987
|
+
if (had) {
|
|
988
|
+
this.evictedCount--;
|
|
989
|
+
}
|
|
990
|
+
if (inner.size === 0) {
|
|
991
|
+
this.evicted.delete(name);
|
|
992
|
+
}
|
|
993
|
+
return had;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// The peer changed the table size it advertises for its own decoder, which is the
|
|
997
|
+
// bound on ours. Any change has to be restated to it with a size update -- that is what
|
|
998
|
+
// nghttp2 does on receiving this setting -- because its decompression context is sized
|
|
999
|
+
// by that number and it has no other way of learning where ours ended up. An unchanged
|
|
1000
|
+
// value, which is every normal SETTINGS, costs nothing.
|
|
1001
|
+
setPeerTableSize(peerSize) {
|
|
1002
|
+
const want = Math.min(peerSize, this.cap);
|
|
1003
|
+
if (peerSize !== this.peerSize || want !== this.maxSize) {
|
|
1004
|
+
this.pendingSize = want;
|
|
1005
|
+
}
|
|
1006
|
+
this.peerSize = peerSize;
|
|
1007
|
+
if (want === this.maxSize) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
this.maxSize = want;
|
|
1011
|
+
let moved = false;
|
|
1012
|
+
while (this.size > want && this.dyn.length > this.base) {
|
|
1013
|
+
const old = this.dyn[this.base++];
|
|
1014
|
+
this.size -= old[0].length + old[1].length + 32;
|
|
1015
|
+
moved = true;
|
|
1016
|
+
}
|
|
1017
|
+
if (moved) {
|
|
1018
|
+
this.touch();
|
|
1019
|
+
} // the indices the caches name just shifted
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// The size update octets for the next field block, or null. Consumed here so a block
|
|
1023
|
+
// never announces the same change twice.
|
|
1024
|
+
takeSizeUpdate() {
|
|
1025
|
+
const n = this.pendingSize;
|
|
1026
|
+
if (n === null) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
this.pendingSize = null;
|
|
1030
|
+
const b = Buffer.allocUnsafe(5);
|
|
1031
|
+
return b.subarray(0, writeInt(b, 0, n, 5, 0x20));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
touch() {
|
|
1035
|
+
this.epoch++;
|
|
1036
|
+
this.cache.clear();
|
|
1037
|
+
this.cacheNever.clear();
|
|
1038
|
+
this.cacheCount = 0;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// The bytes of one field line, or undefined when they have to be built. Two
|
|
1042
|
+
// lookups, no key to build and no key to hash.
|
|
1043
|
+
lookup(name, value, neverIndex) {
|
|
1044
|
+
const inner = (neverIndex ? this.cacheNever : this.cache).get(name);
|
|
1045
|
+
return inner === undefined ? undefined : inner.get(value);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Appends one field line to `dst` at `off`, returns the new offset.
|
|
1049
|
+
// Cached bytes are only valid for the table state that produced them: see touch().
|
|
1050
|
+
write(dst, off, name, value, neverIndex) {
|
|
1051
|
+
const bytes = this.lookup(name, value, neverIndex);
|
|
1052
|
+
if (bytes !== undefined) {
|
|
1053
|
+
return this.put(dst, off, bytes);
|
|
1054
|
+
}
|
|
1055
|
+
return this.emit(dst, off, name, value, neverIndex);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Copies bytes into the destination and returns the new offset. The check belongs
|
|
1059
|
+
// here because the destination is a shared scratch block sized by an upper bound,
|
|
1060
|
+
// so a line that does not fit is a bug in that bound, not in the peer.
|
|
1061
|
+
put(dst, off, bytes) {
|
|
1062
|
+
if (off + bytes.length > dst.length) {
|
|
1063
|
+
throw new HPackError('hpack: output buffer too small');
|
|
1064
|
+
}
|
|
1065
|
+
// set() against copy(): the same memcpy with less argument juggling, and the
|
|
1066
|
+
// difference is real at the size of a field line (OPTIMIZATION.md item 12).
|
|
1067
|
+
dst.set(bytes, off);
|
|
1068
|
+
return off + bytes.length;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// One field line, cold: choose the representation, and the table's fate with it.
|
|
1072
|
+
// Appends the line to `dst` at `off` and returns the new offset. The forms that are
|
|
1073
|
+
// cached (table-independent, or valid for this epoch) are built in a buffer of their
|
|
1074
|
+
// own and copied in; the two that are deliberately not cached -- the first sighting
|
|
1075
|
+
// of a pair, and the insertion that follows it -- are written straight into `dst`,
|
|
1076
|
+
// so a line whose value is new to the response costs no buffer at all.
|
|
1077
|
+
emit(dst, off, name, value, neverIndex) {
|
|
1078
|
+
// 11xxxxxx: the value is named in full and neither table stores it. Emitted as a
|
|
1079
|
+
// literal even when the pair is in the static table, which is what node's
|
|
1080
|
+
// nghttp2 does for sensitive values too -- the point is that nothing is remembered.
|
|
1081
|
+
if (neverIndex) {
|
|
1082
|
+
const bytes = this.literal(name, value, 0x10);
|
|
1083
|
+
this.remember(name, value, neverIndex, bytes);
|
|
1084
|
+
return this.put(dst, off, bytes);
|
|
1085
|
+
}
|
|
1086
|
+
const hit = this.fullIndex(name, value);
|
|
1087
|
+
if (hit !== undefined) {
|
|
1088
|
+
const bytes = this.indexed(hit);
|
|
1089
|
+
this.remember(name, value, neverIndex, bytes);
|
|
1090
|
+
return this.put(dst, off, bytes);
|
|
1091
|
+
}
|
|
1092
|
+
// Its name can never reach the table -- insert() is only reached past this line --
|
|
1093
|
+
// so the bytes reference nothing but static indices and are safe to cache despite
|
|
1094
|
+
// being literals.
|
|
1095
|
+
if (NEVER_INSERT.has(name)) {
|
|
1096
|
+
const bytes = this.literal(name, value, 0x00);
|
|
1097
|
+
this.remember(name, value, neverIndex, bytes);
|
|
1098
|
+
return this.put(dst, off, bytes);
|
|
1099
|
+
}
|
|
1100
|
+
// Nothing may be added to the table: either the peer has no room for it (inserting
|
|
1101
|
+
// would still be correct on the wire, since both tables empty at once, but it would
|
|
1102
|
+
// bump the epoch and clear the caches on every response), or this session gave up on
|
|
1103
|
+
// its table because its working set does not fit. Entries already in it keep their
|
|
1104
|
+
// one-octet references above -- only new ones stop. From here the table stands still,
|
|
1105
|
+
// so these bytes, which may name a dynamic *name* index, stay valid for the session.
|
|
1106
|
+
if (this.maxSize === 0 || this.noInsert) {
|
|
1107
|
+
const bytes = this.literal(name, value, 0x00);
|
|
1108
|
+
this.remember(name, value, neverIndex, bytes);
|
|
1109
|
+
return this.put(dst, off, bytes);
|
|
1110
|
+
}
|
|
1111
|
+
// One more rule taken from hd_deflate_decide_indexing(): an entry bigger than three
|
|
1112
|
+
// quarters of the table is not inserted at all. It would evict nearly the whole table
|
|
1113
|
+
// to make room for itself, and a single reference is worth less than the stable
|
|
1114
|
+
// entries it displaces -- which is the same reasoning as NEVER_INSERT above, applied
|
|
1115
|
+
// by size instead of by name.
|
|
1116
|
+
if (name.length + value.length + 32 > this.maxSize - (this.maxSize >> 2)) {
|
|
1117
|
+
const bytes = this.literal(name, value, 0x00);
|
|
1118
|
+
this.remember(name, value, neverIndex, bytes);
|
|
1119
|
+
return this.put(dst, off, bytes);
|
|
1120
|
+
}
|
|
1121
|
+
// Insert on the second sighting. The first one pays a literal and is only
|
|
1122
|
+
// remembered, so a value that is unique to every response never enters the table,
|
|
1123
|
+
// never evicts, and never clears the cache. Not cached: caching it here is what
|
|
1124
|
+
// would keep it a literal forever. Written straight into the destination, because
|
|
1125
|
+
// this line is thrown away with the response anyway.
|
|
1126
|
+
if (this.onceCount >= CACHE_LIMIT) {
|
|
1127
|
+
this.once.clear();
|
|
1128
|
+
this.onceCount = 0;
|
|
1129
|
+
}
|
|
1130
|
+
let seen = this.once.get(name);
|
|
1131
|
+
if (seen === undefined) {
|
|
1132
|
+
seen = new Set();
|
|
1133
|
+
this.once.set(name, seen);
|
|
1134
|
+
}
|
|
1135
|
+
if (seen.size >= ONCE_PER_NAME) {
|
|
1136
|
+
// This name has produced ONCE_PER_NAME distinct values and none of them came
|
|
1137
|
+
// back, so its values are unique to the response. Stop remembering them: the
|
|
1138
|
+
// check is the cost (the hash of a fresh value string) and the pair would not
|
|
1139
|
+
// be inserted on a second sighting anyway -- that is the rule this name wants.
|
|
1140
|
+
this.unsettled = true;
|
|
1141
|
+
return this.literalInto(dst, off, name, value, 0x00);
|
|
1142
|
+
}
|
|
1143
|
+
if (!seen.has(value)) {
|
|
1144
|
+
seen.add(value);
|
|
1145
|
+
this.onceCount++;
|
|
1146
|
+
this.unsettled = true;
|
|
1147
|
+
return this.literalInto(dst, off, name, value, 0x00);
|
|
1148
|
+
}
|
|
1149
|
+
seen.delete(value);
|
|
1150
|
+
this.onceCount--;
|
|
1151
|
+
// 01xxxxxx: both tables insert. The octets name the table as it is now, so they
|
|
1152
|
+
// are written before insert() moves it.
|
|
1153
|
+
const end = this.literalInto(dst, off, name, value, 0x40);
|
|
1154
|
+
this.insert(name, value);
|
|
1155
|
+
return end; // not cached, on purpose
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Static table first, then ours: the index a peer can resolve without any literal.
|
|
1159
|
+
fullIndex(name, value) {
|
|
1160
|
+
const inner = STATIC_FULL.get(name);
|
|
1161
|
+
const stat = inner === undefined ? undefined : inner.get(value);
|
|
1162
|
+
if (stat !== undefined) {
|
|
1163
|
+
return stat;
|
|
1164
|
+
}
|
|
1165
|
+
const dyn = this.findFull(name, value);
|
|
1166
|
+
return dyn === 0 ? undefined : dyn;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
remember(name, value, neverIndex, bytes) {
|
|
1170
|
+
if (bytes === undefined) {
|
|
1171
|
+
return bytes;
|
|
1172
|
+
}
|
|
1173
|
+
const cache = neverIndex ? this.cacheNever : this.cache;
|
|
1174
|
+
let inner = cache.get(name);
|
|
1175
|
+
if (inner === undefined) {
|
|
1176
|
+
inner = new Map();
|
|
1177
|
+
cache.set(name, inner);
|
|
1178
|
+
}
|
|
1179
|
+
if (!inner.has(value)) {
|
|
1180
|
+
inner.set(value, bytes);
|
|
1181
|
+
if (++this.cacheCount >= CACHE_LIMIT) {
|
|
1182
|
+
this.cache.clear();
|
|
1183
|
+
this.cacheNever.clear();
|
|
1184
|
+
this.cacheCount = 0;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return bytes;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
indexed(index) {
|
|
1192
|
+
const b = Buffer.allocUnsafe(4);
|
|
1193
|
+
return b.subarray(0, writeInt(b, 0, index, 7, 0x80));
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// prefix 0x00 = without indexing (4-bit name field), 0x10 = never indexed,
|
|
1197
|
+
// 0x40 = with incremental indexing (6-bit name field). A never-indexed line names
|
|
1198
|
+
// its field in full as well: nothing about the field may be left in a table.
|
|
1199
|
+
literal(name, value, prefix) {
|
|
1200
|
+
this.plan(name, value, prefix);
|
|
1201
|
+
const out = Buffer.allocUnsafe(L_NEED);
|
|
1202
|
+
let off = writeInt(out, 0, L_NAME_IDX, L_BITS, prefix);
|
|
1203
|
+
if (L_NAME_IDX === 0) {
|
|
1204
|
+
out.set(L_NAME_BYTES, off);
|
|
1205
|
+
off += L_NAME_BYTES.length;
|
|
1206
|
+
}
|
|
1207
|
+
this.literalStrInto(out, off, value, L_VALUE_BITS);
|
|
1208
|
+
return out; // the buffer is exactly as long as the line
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// The same line, written straight into the destination: no buffer of the line's
|
|
1212
|
+
// own, and the size is known before a single octet is written, so the destination
|
|
1213
|
+
// can be checked first. Used for the two forms that are not cached.
|
|
1214
|
+
literalInto(dst, off, name, value, prefix) {
|
|
1215
|
+
this.plan(name, value, prefix);
|
|
1216
|
+
if (off + L_NEED > dst.length) {
|
|
1217
|
+
throw new HPackError('hpack: output buffer too small');
|
|
1218
|
+
}
|
|
1219
|
+
let o = writeInt(dst, off, L_NAME_IDX, L_BITS, prefix);
|
|
1220
|
+
if (L_NAME_IDX === 0) {
|
|
1221
|
+
dst.set(L_NAME_BYTES, o);
|
|
1222
|
+
o += L_NAME_BYTES.length;
|
|
1223
|
+
}
|
|
1224
|
+
o = this.literalStrInto(dst, o, value, L_VALUE_BITS);
|
|
1225
|
+
return o;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// The shape of the line, in the module-level slots above.
|
|
1229
|
+
plan(name, value, prefix) {
|
|
1230
|
+
L_BITS = prefix === 0x40 ? 6 : 4;
|
|
1231
|
+
let nameIdx = 0;
|
|
1232
|
+
if (prefix !== 0x10) {
|
|
1233
|
+
const stat = NAME_INDEX.get(name);
|
|
1234
|
+
nameIdx = stat !== undefined ? stat : this.findName(name);
|
|
1235
|
+
}
|
|
1236
|
+
L_NAME_IDX = nameIdx;
|
|
1237
|
+
L_VALUE_BITS = huffBitsOf(value);
|
|
1238
|
+
if (nameIdx === 0) {
|
|
1239
|
+
// the name is written in full: its octets are the cached ones
|
|
1240
|
+
L_NAME_BYTES = nameBytes(name);
|
|
1241
|
+
L_NEED = intLen(0, L_BITS) + L_NAME_BYTES.length + strLen(value, L_VALUE_BITS);
|
|
1242
|
+
} else {
|
|
1243
|
+
L_NAME_BYTES = null;
|
|
1244
|
+
L_NEED = intLen(nameIdx, L_BITS) + strLen(value, L_VALUE_BITS);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// One literal string: its length prefix and the string itself, Huffman-coded when
|
|
1249
|
+
// that is shorter. The octets of a string are its latin1 bytes -- one per code
|
|
1250
|
+
// unit, the low byte of each -- so the Huffman form is coded straight from the
|
|
1251
|
+
// string and the raw form goes through latin1Into().
|
|
1252
|
+
literalStrInto(dst, off, s, huffBits) {
|
|
1253
|
+
const rawLen = s.length;
|
|
1254
|
+
const hlen = (huffBits + 7) >> 3;
|
|
1255
|
+
if (hlen < rawLen) { // Huffman wins
|
|
1256
|
+
off = writeInt(dst, off, hlen, 7, 0x80);
|
|
1257
|
+
return huffEncodeStr(s, dst, off);
|
|
1258
|
+
}
|
|
1259
|
+
off = writeInt(dst, off, rawLen, 7, 0x00);
|
|
1260
|
+
return latin1Into(dst, off, s);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
sizeUpdate(dst, off, size) {
|
|
1265
|
+
return writeInt(dst, off, size, 5, 0x20);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Upper bound on the bytes one field line can produce, for capacity planning.
|
|
1269
|
+
static maxBytesOf(name, value) {
|
|
1270
|
+
return 1 + 4 + (name.length + 5) * 2 + (value.length + 5) * 2;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
module.exports = {
|
|
1275
|
+
Decoder,
|
|
1276
|
+
Encoder,
|
|
1277
|
+
HPackError,
|
|
1278
|
+
STATIC,
|
|
1279
|
+
FULL_INDEX,
|
|
1280
|
+
NAME_INDEX,
|
|
1281
|
+
nameBytesSize,
|
|
1282
|
+
huffEncode,
|
|
1283
|
+
huffEncodeStr,
|
|
1284
|
+
huffDecode,
|
|
1285
|
+
huffBitLen,
|
|
1286
|
+
writeInt,
|
|
1287
|
+
readInt
|
|
1288
|
+
};
|