1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.handler.codec.http3;
17
18 import io.netty.util.AsciiString;
19 import org.jetbrains.annotations.Nullable;
20
21 import static io.netty.handler.codec.http3.QpackHeaderField.ENTRY_OVERHEAD;
22 import static io.netty.handler.codec.http3.QpackUtil.MAX_HEADER_TABLE_SIZE;
23 import static io.netty.handler.codec.http3.QpackUtil.MIN_HEADER_TABLE_SIZE;
24 import static io.netty.handler.codec.http3.QpackUtil.equalsVariableTime;
25 import static io.netty.util.AsciiString.EMPTY_STRING;
26 import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
27 import static java.lang.Math.max;
28 import static java.lang.Math.min;
29 import static java.lang.Math.toIntExact;
30
31 final class QpackEncoderDynamicTable {
32 private static final QpackException INVALID_KNOW_RECEIVED_COUNT_INCREMENT =
33 QpackException.newStatic(QpackDecoder.class, "incrementKnownReceivedCount(...)",
34 "QPACK - invalid known received count increment.");
35 private static final QpackException INVALID_REQUIRED_INSERT_COUNT_INCREMENT =
36 QpackException.newStatic(QpackDecoder.class, "acknowledgeInsertCount(...)",
37 "QPACK - invalid required insert count acknowledgment.");
38 private static final QpackException INVALID_TABLE_CAPACITY =
39 QpackException.newStatic(QpackDecoder.class, "validateCapacity(...)",
40 "QPACK - dynamic table capacity is invalid.");
41 private static final QpackException CAPACITY_ALREADY_SET =
42 QpackException.newStatic(QpackDecoder.class, "maxTableCapacity(...)",
43 "QPACK - dynamic table capacity is already set.");
44
45
46
47 public static final int NOT_FOUND = Integer.MIN_VALUE;
48
49
50
51
52 private final HeaderEntry[] fields;
53
54
55
56
57 private final int expectedFreeCapacityPercentage;
58
59
60
61
62 private final byte hashMask;
63
64
65
66
67 private long size;
68
69
70
71
72
73
74 private long maxTableCapacity = -1;
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 private final HeaderEntry head;
96
97
98
99
100
101 private HeaderEntry drain;
102
103
104
105
106
107
108 private HeaderEntry knownReceived;
109
110
111
112
113 private HeaderEntry tail;
114
115 QpackEncoderDynamicTable() {
116 this(16, 10);
117 }
118
119 QpackEncoderDynamicTable(int arraySizeHint, int expectedFreeCapacityPercentage) {
120
121
122 fields = new HeaderEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))];
123 hashMask = (byte) (fields.length - 1);
124
125
126 head = new HeaderEntry(-1, EMPTY_STRING, EMPTY_STRING, -1, null);
127 this.expectedFreeCapacityPercentage = expectedFreeCapacityPercentage;
128 resetIndicesToHead();
129 }
130
131
132
133
134
135
136
137
138
139 int add(CharSequence name, CharSequence value, long headerSize) {
140 if (maxTableCapacity - size < headerSize) {
141 return -1;
142 }
143
144 if (tail.index == Integer.MAX_VALUE) {
145
146 evictUnreferencedEntries();
147 return -1;
148 }
149 int h = AsciiString.hashCode(name);
150 int i = index(h);
151 HeaderEntry old = fields[i];
152 HeaderEntry e = new HeaderEntry(h, name, value, tail.index + 1, old);
153 fields[i] = e;
154 e.addNextTo(tail);
155 tail = e;
156 size += headerSize;
157
158 ensureFreeCapacity();
159 return e.index;
160 }
161
162
163
164
165
166
167
168
169
170 void acknowledgeInsertCountOnAck(int entryIndex) throws QpackException {
171 acknowledgeInsertCount(entryIndex, true);
172 }
173
174
175
176
177
178
179
180
181
182 void acknowledgeInsertCountOnCancellation(int entryIndex) throws QpackException {
183 acknowledgeInsertCount(entryIndex, false);
184 }
185
186 private void acknowledgeInsertCount(int entryIndex, boolean updateKnownReceived) throws QpackException {
187 if (entryIndex < 0) {
188 throw INVALID_REQUIRED_INSERT_COUNT_INCREMENT;
189 }
190 for (HeaderEntry e = head.next; e != null; e = e.next) {
191 if (e.index == entryIndex) {
192 assert e.refCount > 0;
193 e.refCount--;
194 if (updateKnownReceived && e.index > knownReceived.index) {
195
196
197
198 knownReceived = e;
199 }
200 evictUnreferencedEntries();
201 return;
202 }
203 }
204
205
206
207 throw INVALID_REQUIRED_INSERT_COUNT_INCREMENT;
208 }
209
210
211
212
213
214
215
216
217
218 void incrementKnownReceivedCount(int knownReceivedCountIncr) throws QpackException {
219 if (knownReceivedCountIncr <= 0) {
220 throw INVALID_KNOW_RECEIVED_COUNT_INCREMENT;
221 }
222 while (knownReceived.next != null && knownReceivedCountIncr > 0) {
223 knownReceived = knownReceived.next;
224 knownReceivedCountIncr--;
225 }
226 if (knownReceivedCountIncr == 0) {
227 evictUnreferencedEntries();
228 return;
229 }
230
231
232
233 throw INVALID_KNOW_RECEIVED_COUNT_INCREMENT;
234 }
235
236
237
238
239
240
241 int insertCount() {
242 return tail.index + 1;
243 }
244
245
246
247
248
249
250
251 int encodedRequiredInsertCount(int reqInsertCount) {
252
253
254
255
256
257
258 return reqInsertCount == 0 ? 0 : reqInsertCount % toIntExact(2 * QpackUtil.maxEntries(maxTableCapacity)) + 1;
259 }
260
261
262 int encodedKnownReceivedCount() {
263
264 return encodedRequiredInsertCount(knownReceived.index + 1);
265 }
266
267
268
269
270
271
272 void maxTableCapacity(long capacity) throws QpackException {
273 validateCapacity(capacity);
274 if (this.maxTableCapacity >= 0) {
275 throw CAPACITY_ALREADY_SET;
276 }
277 this.maxTableCapacity = capacity;
278 }
279
280
281
282
283
284
285
286
287
288 int relativeIndexForEncoderInstructions(int entryIndex) {
289 assert entryIndex >= 0;
290 assert entryIndex <= tail.index;
291 return tail.index - entryIndex;
292 }
293
294
295
296
297
298
299
300
301
302
303 int getEntryIndex(@Nullable CharSequence name, @Nullable CharSequence value) {
304 if (tail != head && name != null && value != null) {
305 int h = AsciiString.hashCode(name);
306 int i = index(h);
307 HeaderEntry firstNameMatch = null;
308 HeaderEntry entry = null;
309 for (HeaderEntry e = fields[i]; e != null; e = e.nextSibling) {
310 if (e.hash == h && equalsVariableTime(value, e.value)) {
311 if (equalsVariableTime(name, e.name)) {
312 entry = e;
313 break;
314 }
315 } else if (firstNameMatch == null && equalsVariableTime(name, e.name)) {
316 firstNameMatch = e;
317 }
318 }
319 if (entry != null) {
320 return entry.index;
321 }
322 if (firstNameMatch != null) {
323 return -firstNameMatch.index - 1;
324 }
325 }
326 return NOT_FOUND;
327 }
328
329
330
331
332
333
334
335
336
337
338 int addReferenceToEntry(@Nullable CharSequence name, @Nullable CharSequence value, int idx) {
339 if (tail != head && name != null && value != null) {
340 int h = AsciiString.hashCode(name);
341 int i = index(h);
342 for (HeaderEntry e = fields[i]; e != null; e = e.nextSibling) {
343 if (e.hash == h && idx == e.index) {
344 e.refCount++;
345 return e.index + 1;
346 }
347 }
348 }
349 throw new IllegalArgumentException("Index " + idx + " not found");
350 }
351
352 boolean requiresDuplication(int idx, long size) {
353 assert head != tail;
354
355 if (this.size + size > maxTableCapacity || head == drain) {
356 return false;
357 }
358 return idx >= head.next.index && idx <= drain.index;
359 }
360
361 private void evictUnreferencedEntries() {
362 if (head == knownReceived || head == drain) {
363 return;
364 }
365
366 while (head.next != null && head.next != knownReceived.next && head.next != drain.next) {
367 if (!removeIfUnreferenced()) {
368 return;
369 }
370 }
371 }
372
373 private boolean removeIfUnreferenced() {
374 final HeaderEntry toRemove = head.next;
375 if (toRemove.refCount != 0) {
376 return false;
377 }
378 size -= toRemove.size();
379
380
381 final int i = index(toRemove.hash);
382 HeaderEntry e = fields[i];
383 HeaderEntry prev = null;
384 while (e != null && e != toRemove) {
385 prev = e;
386 e = e.nextSibling;
387 }
388 if (e == toRemove) {
389 if (prev == null) {
390 fields[i] = e.nextSibling;
391 } else {
392 prev.nextSibling = e.nextSibling;
393 }
394 }
395
396
397 toRemove.remove(head);
398 if (toRemove == tail) {
399 resetIndicesToHead();
400 }
401 if (toRemove == drain) {
402 drain = head;
403 }
404 if (toRemove == knownReceived) {
405 knownReceived = head;
406 }
407 return true;
408 }
409
410 private void resetIndicesToHead() {
411 tail = head;
412 drain = head;
413 knownReceived = head;
414 }
415
416 private void ensureFreeCapacity() {
417 long maxDesiredSize = max(ENTRY_OVERHEAD, ((100 - expectedFreeCapacityPercentage) * maxTableCapacity) / 100);
418 long cSize = size;
419 HeaderEntry nDrain;
420 for (nDrain = head; nDrain.next != null && cSize > maxDesiredSize; nDrain = nDrain.next) {
421 cSize -= nDrain.next.size();
422 }
423 if (cSize != size) {
424 drain = nDrain;
425 evictUnreferencedEntries();
426 }
427 }
428
429 private int index(int h) {
430 return h & hashMask;
431 }
432
433 private static void validateCapacity(long capacity) throws QpackException {
434 if (capacity < MIN_HEADER_TABLE_SIZE || capacity > MAX_HEADER_TABLE_SIZE) {
435 throw INVALID_TABLE_CAPACITY;
436 }
437 }
438
439
440
441
442 private static final class HeaderEntry extends QpackHeaderField {
443
444
445
446 HeaderEntry next;
447
448
449
450
451 HeaderEntry nextSibling;
452
453
454
455
456
457
458 int refCount;
459
460
461
462
463 final int hash;
464
465
466
467
468 final int index;
469
470 HeaderEntry(int hash, CharSequence name, CharSequence value, int index, @Nullable HeaderEntry nextSibling) {
471 super(name, value);
472 this.index = index;
473 this.hash = hash;
474 this.nextSibling = nextSibling;
475 }
476
477 void remove(HeaderEntry prev) {
478 assert prev != this;
479 prev.next = next;
480 next = null;
481 nextSibling = null;
482 }
483
484 void addNextTo(HeaderEntry prev) {
485 assert prev != this;
486 this.next = prev.next;
487 prev.next = this;
488 }
489 }
490 }