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 Http3SettingIdentifier#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     /**
353      * Returns {@code true} if the decoder acknowledged the entry at the passed {@code idx}, which means it can be
354      * referenced in a field section without blocking the stream.
355      * See <a href="https://www.rfc-editor.org/rfc/rfc9204.html#name-known-received-count">known received count</a>.
356      *
357      * @param idx of the entry.
358      * @return {@code true} if the entry was acknowledged by the decoder.
359      */
360     boolean isKnownReceived(int idx) {
361         return idx <= knownReceived.index;
362     }
363 
364     boolean requiresDuplication(int idx, long size) {
365         assert head != tail;
366 
367         if (this.size + size > maxTableCapacity || head == drain) {
368             return false;
369         }
370         return idx >= head.next.index && idx <= drain.index;
371     }
372 
373     private void evictUnreferencedEntries() {
374         if (head == knownReceived || head == drain) {
375             return;
376         }
377 
378         while (head.next != null && head.next != knownReceived.next && head.next != drain.next) {
379             if (!removeIfUnreferenced()) {
380                 return;
381             }
382         }
383     }
384 
385     private boolean removeIfUnreferenced() {
386         final HeaderEntry toRemove = head.next;
387         if (toRemove.refCount != 0) {
388             return false;
389         }
390         size -= toRemove.size();
391 
392         // Remove from the hash map
393         final int i = index(toRemove.hash);
394         HeaderEntry e = fields[i];
395         HeaderEntry prev = null;
396         while (e != null && e != toRemove) {
397             prev = e;
398             e = e.nextSibling;
399         }
400         if (e == toRemove) {
401             if (prev == null) {
402                 fields[i] = e.nextSibling;
403             } else {
404                 prev.nextSibling = e.nextSibling;
405             }
406         }
407 
408         // Remove from the linked list
409         toRemove.remove(head);
410         if (toRemove == tail) {
411             resetIndicesToHead();
412         }
413         if (toRemove == drain) {
414             drain = head;
415         }
416         if (toRemove == knownReceived) {
417             knownReceived = head;
418         }
419         return true;
420     }
421 
422     private void resetIndicesToHead() {
423         tail = head;
424         drain = head;
425         knownReceived = head;
426     }
427 
428     private void ensureFreeCapacity() {
429         long maxDesiredSize = max(ENTRY_OVERHEAD, ((100 - expectedFreeCapacityPercentage) * maxTableCapacity) / 100);
430         long cSize = size;
431         HeaderEntry nDrain;
432         for (nDrain = head; nDrain.next != null && cSize > maxDesiredSize; nDrain = nDrain.next) {
433             cSize -= nDrain.next.size();
434         }
435         if (cSize != size) {
436             drain = nDrain;
437             evictUnreferencedEntries();
438         }
439     }
440 
441     private int index(int h) {
442         return h & hashMask;
443     }
444 
445     private static void validateCapacity(long capacity) throws QpackException {
446         if (capacity < MIN_HEADER_TABLE_SIZE || capacity > MAX_HEADER_TABLE_SIZE) {
447             throw INVALID_TABLE_CAPACITY;
448         }
449     }
450 
451     /**
452      * An entry for the {@link #fields} HashMap. This entry provides insertion order iteration using {@link #next}.
453      */
454     private static final class HeaderEntry extends QpackHeaderField {
455         /**
456          * Pointer to the next entry in insertion order with a different {@link #hash} than this entry.
457          */
458         HeaderEntry next;
459 
460         /**
461          * Pointer to the next entry in insertion order with the same {@link #hash} as this entry, a.k.a hash collisions
462          */
463         HeaderEntry nextSibling;
464 
465         /**
466          * Number of header blocks that refer to this entry as the value for its <a
467          * href="https://www.rfc-editor.org/rfc/rfc9204.html#name-required-insert-count">
468          * required insert count</a>
469          */
470         int refCount;
471 
472         /**
473          * Hashcode for this entry.
474          */
475         final int hash;
476 
477         /**
478          * Insertion index for this entry.
479          */
480         final int index;
481 
482         HeaderEntry(int hash, CharSequence name, CharSequence value, int index, @Nullable HeaderEntry nextSibling) {
483             super(name, value);
484             this.index = index;
485             this.hash = hash;
486             this.nextSibling = nextSibling;
487         }
488 
489         void remove(HeaderEntry prev) {
490             assert prev != this;
491             prev.next = next;
492             next = null; // null references to prevent nepotism in generational GC.
493             nextSibling = null;
494         }
495 
496         void addNextTo(HeaderEntry prev) {
497             assert prev != this;
498             this.next = prev.next;
499             prev.next = this;
500         }
501     }
502 }