1 /*
2 * Copyright 2021 The Netty Project
3 *
4 * The Netty Project licenses this file to you under the Apache License,
5 * version 2.0 (the "License"); you may not use this file except in compliance
6 * with the License. You may obtain a copy of the License at:
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
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 * Special return value of {@link #getEntryIndex(CharSequence, CharSequence)} when the entry is not found.
46 */
47 public static final int NOT_FOUND = Integer.MIN_VALUE;
48
49 /**
50 * A hashmap of header entries.
51 */
52 private final HeaderEntry[] fields;
53
54 /**
55 * Percentage of capacity that we expect to be free after eviction of old entries.
56 */
57 private final int expectedFreeCapacityPercentage;
58
59 /**
60 * Hash mask for all entries in the hashmap.
61 */
62 private final byte hashMask;
63
64 /**
65 * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-dynamic-table-size">Current size of the table</a>.
66 */
67 private long size;
68
69 /**
70 * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-maximum-dynamic-table-capac">
71 * Maximum capacity of the table</a>. This is set once based on the
72 * {@link Http3SettingsFrame#HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY} received by the remote peer.
73 */
74 private long maxTableCapacity = -1;
75
76 /*
77 * The below indexes follow the suggested heuristics in Section 2.1.1.1 Avoiding Prohibited insertions
78 * https://www.rfc-editor.org/rfc/rfc9204.html#name-avoiding-prohibited-inserti
79 *
80 * Tail Drain Head
81 * | | |
82 * v v v
83 * +--------+---------------------------------+----------+
84 * | Unused | Referenceable | Draining |
85 * | Space | Entries | Entries |
86 * +--------+---------------------------------+----------+
87 * ^ ^ ^
88 * | | |
89 * Insertion Index Draining Index Dropping Index
90 */
91
92 /**
93 * Head of the entries, such that {@link HeaderEntry#index} is the {@code droppingIndex}.
94 */
95 private final HeaderEntry head;
96
97 /**
98 * Pointer before which entries are marked for eviction post {@link #incrementKnownReceivedCount(int)}.
99 * {@link HeaderEntry#index} is the {@code drainingIndex}.
100 */
101 private HeaderEntry drain;
102
103 /**
104 * Pointer to the entry representing the <a
105 * href="https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-known-received-count">
106 * known received count</a>.
107 */
108 private HeaderEntry knownReceived;
109
110 /**
111 * Tail of the entries, such that {@link HeaderEntry#index} is the {@code insertionIndex}.
112 */
113 private HeaderEntry tail;
114
115 QpackEncoderDynamicTable() {
116 this(16, 10);
117 }
118
119 QpackEncoderDynamicTable(int arraySizeHint, int expectedFreeCapacityPercentage) {
120 // Enforce a bound of [2, 128] because hashMask is a byte. The max possible value of hashMask is one less
121 // than the length of this array, and we want the mask to be > 0.
122 fields = new HeaderEntry[findNextPositivePowerOfTwo(max(2, min(arraySizeHint, 128)))];
123 hashMask = (byte) (fields.length - 1);
124 // Start with index -1 so the first added header will have the index of 0.
125 // See https://www.rfc-editor.org/rfc/rfc9204.html#name-absolute-indexing
126 head = new HeaderEntry(-1, EMPTY_STRING, EMPTY_STRING, -1, null);
127 this.expectedFreeCapacityPercentage = expectedFreeCapacityPercentage;
128 resetIndicesToHead();
129 }
130
131 /**
132 * Add a name - value pair to the dynamic table and returns the index.
133 *
134 * @param name the name.
135 * @param value the value.
136 * @param headerSize the size of the header.
137 * @return the absolute index or {@code -1) if it could not be added.
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 // Wait for all entries to evict before we restart indexing from zero
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 * Callback when a header block which had a {@link #insertCount()}} greater than {@code 0} is
164 * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-section-acknowledgment">acknowledged</a>
165 * by the decoder.
166 *
167 * @param entryIndex For the entry corresponding to the {@link #insertCount()}.
168 * @throws QpackException If the count is invalid.
169 */
170 void acknowledgeInsertCountOnAck(int entryIndex) throws QpackException {
171 acknowledgeInsertCount(entryIndex, true);
172 }
173
174 /**
175 * Callback when a header block which had a {@link #insertCount()}} greater than {@code 0} is still not processed
176 * and the stream is <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-stream-cancellation">cancelled</a>
177 * by the decoder.
178 *
179 * @param entryIndex For the entry corresponding to the {@link #insertCount()}.
180 * @throws QpackException If the count is invalid.
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 // https://www.rfc-editor.org/rfc/rfc9204.html#name-known-received-count
196 // If the Required Insert Count of the acknowledged field section is greater than the current Known
197 // Received Count, Known Received Count is updated to that Required Insert Count value.
198 knownReceived = e;
199 }
200 evictUnreferencedEntries();
201 return;
202 }
203 }
204 // We have reached the end of the linked list so the index was invalid and hence the connection should
205 // be closed.
206 // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.4
207 throw INVALID_REQUIRED_INSERT_COUNT_INCREMENT;
208 }
209
210 /**
211 * Callback when a decoder <a
212 * href="https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-count-increment">increments its
213 * insert count.</a>
214 *
215 * @param knownReceivedCountIncr Increment count.
216 * @throws QpackException If the increment count is invalid.
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 // We have reached the end of the linked list so the index was invalid and hence the connection should be
231 // closed.
232 // https://www.rfc-editor.org/rfc/rfc9204.html#name-decoder-instructions
233 throw INVALID_KNOW_RECEIVED_COUNT_INCREMENT;
234 }
235
236 /**
237 * Returns the number of entries inserted to this dynamic table.
238 *
239 * @return number the added entries.
240 */
241 int insertCount() {
242 return tail.index + 1;
243 }
244
245 /**
246 * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count">
247 * Encodes the required insert count.</a>
248 * @param reqInsertCount the required insert count.
249 * @return the encoded count.
250 */
251 int encodedRequiredInsertCount(int reqInsertCount) {
252 // https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count
253 // if ReqInsertCount == 0:
254 // EncInsertCount = 0
255 // else:
256 // EncInsertCount = (ReqInsertCount mod (2 * MaxEntries)) + 1
257 //
258 return reqInsertCount == 0 ? 0 : reqInsertCount % toIntExact(2 * QpackUtil.maxEntries(maxTableCapacity)) + 1;
259 }
260
261 // Visible for tests
262 int encodedKnownReceivedCount() {
263 // https://www.rfc-editor.org/rfc/rfc9204.html#name-known-received-count
264 return encodedRequiredInsertCount(knownReceived.index + 1);
265 }
266
267 /**
268 * Set the maximum capacity of the dynamic table. This can only be set once.
269 * @param capacity the capacity
270 * @throws QpackException if capacity was set before.
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 * Transforms the passed {@code entryIndex} as a <a
282 * href="https://www.rfc-editor.org/rfc/rfc9204.html#name-relative-indexing">relative index for
283 * encoder instructions</a>.
284 *
285 * @param entryIndex to transform.
286 * @return Relative index for the passed {@code entryIndex}.
287 */
288 int relativeIndexForEncoderInstructions(int entryIndex) {
289 assert entryIndex >= 0;
290 assert entryIndex <= tail.index;
291 return tail.index - entryIndex;
292 }
293
294 /**
295 * Finds an entry with the passed {@code name} and {@code value} in this dynamic table.
296 *
297 * @param name of the entry to find.
298 * @param value of the entry to find.
299 * @return {@link #NOT_FOUND} if the entry does not exist. If an entry with matching {@code name} and {@code value}
300 * exists, then the index is returned. If an entry with only matching name exists then {@code -index-1} is
301 * returned.
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 * Adds a reference to an entry at the passed {@code idx}.
331 *
332 * @param name of the entry for lookups, not verified for the entry at the pased {@code idx}
333 * @param value of the entry for lookups, not verified for the entry at the pased {@code idx}
334 * @param idx of the entry.
335 * @return <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count">Required
336 * insert count</a> if the passed entry has to be referenced in a header block.
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 // Remove from the hash map
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 // Remove from the linked list
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 * An entry for the {@link #fields} HashMap. This entry provides insertion order iteration using {@link #next}.
441 */
442 private static final class HeaderEntry extends QpackHeaderField {
443 /**
444 * Pointer to the next entry in insertion order with a different {@link #hash} than this entry.
445 */
446 HeaderEntry next;
447
448 /**
449 * Pointer to the next entry in insertion order with the same {@link #hash} as this entry, a.k.a hash collisions
450 */
451 HeaderEntry nextSibling;
452
453 /**
454 * Number of header blocks that refer to this entry as the value for its <a
455 * href="https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count">
456 * required insert count</a>
457 */
458 int refCount;
459
460 /**
461 * Hashcode for this entry.
462 */
463 final int hash;
464
465 /**
466 * Insertion index for this entry.
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; // null references to prevent nepotism in generational GC.
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 }