View Javadoc
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 }