View Javadoc
1   /*
2    * Copyright 2020 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.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufAllocator;
20  import io.netty.handler.codec.quic.QuicStreamChannel;
21  import io.netty.util.ReferenceCountUtil;
22  import io.netty.util.collection.LongObjectHashMap;
23  import io.netty.util.internal.ObjectUtil;
24  
25  import javax.annotation.Nullable;
26  import java.util.ArrayDeque;
27  import java.util.Arrays;
28  import java.util.Map;
29  import java.util.Queue;
30  
31  import static io.netty.handler.codec.http3.Http3CodecUtils.closeOnFailure;
32  import static io.netty.handler.codec.http3.QpackHeaderField.sizeOf;
33  import static io.netty.handler.codec.http3.QpackUtil.encodePrefixedInteger;
34  
35  /**
36   * A QPACK encoder.
37   */
38  final class QpackEncoder {
39      private static final QpackException INVALID_SECTION_ACKNOWLEDGMENT =
40              QpackException.newStatic(QpackDecoder.class, "sectionAcknowledgment(...)",
41                      "QPACK - section acknowledgment received for unknown stream.");
42      private static final int DYNAMIC_TABLE_ENCODE_NOT_DONE = -1;
43      private static final int DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE = -2;
44      /**
45       * Maximum number of field sections for which we will track dynamic-table references while waiting for the
46       * peer's Section Acknowledgment or Stream Cancellation instruction. Both instructions are optional per
47       * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-2.2.2.2">RFC 9204, section 2.2.2.2</a>, so a
48       * remote peer that never sends them (while still acknowledging dynamic-table insertions) must not be able to
49       * grow this per-connection state without bound. Once the limit is reached we stop referencing the dynamic
50       * table for new field sections (falling back to literal encoding, which is always legal, see
51       * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.4">section 4.5.4</a>) until enough
52       * outstanding sections are acknowledged or cancelled to free up tracking capacity again.
53       */
54      // Visible for tests
55      static final int MAX_OUTSTANDING_SECTIONS = 10_000;
56  
57      private final QpackHuffmanEncoder huffmanEncoder;
58      private final QpackEncoderDynamicTable dynamicTable;
59      private final QpackSensitivityDetector sensitivityDetector;
60      private int maxBlockedStreams;
61      private int blockedStreams;
62      private LongObjectHashMap<Queue<Indices>> streamSectionTrackers;
63      private int outstandingSections;
64  
65      QpackEncoder(@Nullable QpackSensitivityDetector sensitivityDetector) {
66          this(new QpackEncoderDynamicTable(), sensitivityDetector);
67      }
68  
69      QpackEncoder(QpackEncoderDynamicTable dynamicTable, @Nullable QpackSensitivityDetector sensitivityDetector) {
70          huffmanEncoder = new QpackHuffmanEncoder();
71          this.dynamicTable = ObjectUtil.checkNotNull(dynamicTable, "dynamicTable");
72          this.sensitivityDetector = sensitivityDetector == null ?
73                  QpackSensitivityDetector.NEVER_SENSITIVE : sensitivityDetector;
74      }
75  
76      /**
77       * Encode the header field into the header block.
78       *
79       * <p>Fields for which {@link QpackSensitivityDetector#isSensitive(CharSequence, CharSequence)}
80       * returns {@code true} are encoded as literals with the
81       * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.4">"Never Indexed"</a>
82       * ({@code N=1}) flag set, and are never inserted into the dynamic table.</p>
83       */
84      void encodeHeaders(QpackAttributes qpackAttributes, ByteBuf out, ByteBufAllocator allocator, long streamId,
85                         Http3Headers headers) {
86          final int base = dynamicTable.insertCount();
87          // Allocate a new buffer as we have to go back and write a variable length base and required insert count
88          // later.
89          ByteBuf tmp = allocator.buffer();
90          try {
91              int maxDynamicTblIdx = -1;
92              int requiredInsertCount = 0;
93              Indices dynamicTableIndices = null;
94              for (Map.Entry<CharSequence, CharSequence> header : headers) {
95                  CharSequence name = header.getKey();
96                  CharSequence value = header.getValue();
97                  int dynamicTblIdx;
98                  if (sensitivityDetector.isSensitive(name, value)) {
99                      encodeSensitiveHeader(tmp, name, value);
100                     dynamicTblIdx = DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
101                 } else {
102                     dynamicTblIdx = encodeHeader(qpackAttributes, tmp, base, name, value);
103                 }
104                 if (dynamicTblIdx >= 0) {
105                     int req = dynamicTable.addReferenceToEntry(name, value, dynamicTblIdx);
106                     if (dynamicTblIdx > maxDynamicTblIdx) {
107                         maxDynamicTblIdx = dynamicTblIdx;
108                         requiredInsertCount = req;
109                     }
110                     if (dynamicTableIndices == null) {
111                         dynamicTableIndices = new Indices();
112                     }
113                     dynamicTableIndices.add(dynamicTblIdx);
114                 }
115             }
116 
117             // Track all the indices that we need to ack later.
118             if (dynamicTableIndices != null) {
119                 assert streamSectionTrackers != null;
120                 streamSectionTrackers.computeIfAbsent(streamId, __ -> new ArrayDeque<>())
121                         .add(dynamicTableIndices);
122                 outstandingSections++;
123             }
124 
125             // https://www.rfc-editor.org/rfc/rfc9204.html#name-encoded-field-section-prefi
126             //   0   1   2   3   4   5   6   7
127             // +---+---+---+---+---+---+---+---+
128             // |   Required Insert Count (8+)  |
129             // +---+---------------------------+
130             // | S |      Delta Base (7+)      |
131             // +---+---------------------------+
132             encodePrefixedInteger(out, (byte) 0b0, 8, dynamicTable.encodedRequiredInsertCount(requiredInsertCount));
133             if (base >= requiredInsertCount) {
134                 encodePrefixedInteger(out, (byte) 0b0, 7, base - requiredInsertCount);
135             } else {
136                 encodePrefixedInteger(out, (byte) 0b1000_0000, 7, requiredInsertCount - base - 1);
137             }
138             out.writeBytes(tmp);
139         } finally {
140             tmp.release();
141         }
142     }
143 
144     void configureDynamicTable(QpackAttributes attributes, long maxTableCapacity, int blockedStreams)
145             throws QpackException {
146         if (maxTableCapacity > 0) {
147             assert attributes.encoderStreamAvailable();
148             final QuicStreamChannel encoderStream = attributes.encoderStream();
149             dynamicTable.maxTableCapacity(maxTableCapacity);
150             final ByteBuf tableCapacity = encoderStream.alloc().buffer(8);
151             // https://www.rfc-editor.org/rfc/rfc9204.html#name-set-dynamic-table-capacity
152             //  0   1   2   3   4   5   6   7
153             // +---+---+---+---+---+---+---+---+
154             // | 0 | 0 | 1 |   Capacity (5+)   |
155             // +---+---+---+-------------------+
156             encodePrefixedInteger(tableCapacity, (byte) 0b0010_0000, 5, maxTableCapacity);
157             closeOnFailure(encoderStream.writeAndFlush(tableCapacity));
158 
159             streamSectionTrackers = new LongObjectHashMap<>();
160             maxBlockedStreams = blockedStreams;
161         }
162     }
163 
164     /**
165      * <a href="https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-section-acknowledgment">
166      *     Section acknowledgment</a> for the passed {@code streamId}.
167      *
168      * @param streamId For which the header fields section is acknowledged.
169      */
170     void sectionAcknowledgment(long streamId) throws QpackException {
171         // If a configureDynamicTable(...) was called with a maxTableCapacity of 0 we will have not instanced
172         // streamSectionTrackers. The remote peer might still (incorrectly) send a section acknowledgment for a
173         // stream, so this must be treated as a protocol error instead of throwing an NPE.
174         // See https://www.rfc-editor.org/rfc/rfc9204.html#section-4.4.1
175         if (streamSectionTrackers == null) {
176             throw INVALID_SECTION_ACKNOWLEDGMENT;
177         }
178         final Queue<Indices> tracker = streamSectionTrackers.get(streamId);
179         if (tracker == null) {
180             throw INVALID_SECTION_ACKNOWLEDGMENT;
181         }
182 
183         Indices dynamicTableIndices = tracker.poll();
184 
185         if (tracker.isEmpty()) {
186             streamSectionTrackers.remove(streamId);
187         }
188 
189         if (dynamicTableIndices == null) {
190             throw INVALID_SECTION_ACKNOWLEDGMENT;
191         }
192         outstandingSections--;
193 
194         dynamicTableIndices.forEach(dynamicTable::acknowledgeInsertCountOnAck);
195     }
196 
197     /**
198      * <a href="https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-stream-cancellation">
199      *     Stream cancellation</a> for the passed {@code streamId}.
200      *
201      * @param streamId which is cancelled.
202      */
203     void streamCancellation(long streamId) throws QpackException {
204         // If a configureDynamicTable(...) was called with a maxTableCapacity of 0 we will have not instanced
205         // streamSectionTrackers. The remote peer might still send a stream cancellation for a stream, while it
206         // is optional. See https://www.rfc-editor.org/rfc/rfc9204.html#section-2.2.2.2
207         if (streamSectionTrackers == null) {
208             return;
209         }
210         final Queue<Indices> tracker = streamSectionTrackers.remove(streamId);
211         if (tracker != null) {
212             for (;;) {
213                 Indices dynamicTableIndices = tracker.poll();
214                 if (dynamicTableIndices == null) {
215                     break;
216                 }
217                 outstandingSections--;
218                 dynamicTableIndices.forEach(dynamicTable::acknowledgeInsertCountOnCancellation);
219             }
220         }
221     }
222 
223     /**
224      * <a href="https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-insert-count-increment">
225      *     Insert count increment</a>.
226      *
227      * @param increment for the known received count.
228      */
229     void insertCountIncrement(int increment) throws QpackException {
230         dynamicTable.incrementKnownReceivedCount(increment);
231     }
232 
233     /**
234      * Encode a header field that the {@link QpackSensitivityDetector} flagged as sensitive.
235      *
236      * <p>Sensitive fields are never inserted into the dynamic table and are encoded
237      * with the {@code "Never Indexed"} flag ({@code N=1}) so that intermediaries
238      * also avoid indexing them — see
239      * <a href="https://www.rfc-editor.org/rfc/rfc9204.html#section-7.1">RFC 9204 7.1</a>.
240      * </p>
241      */
242     private void encodeSensitiveHeader(ByteBuf out, CharSequence name, CharSequence value) {
243         final int index = QpackStaticTable.findFieldIndex(name, value);
244         if (index == QpackStaticTable.NOT_FOUND) {
245             encodeLiteral(out, name, value, true);
246         } else if ((index & QpackStaticTable.MASK_NAME_REF) == QpackStaticTable.MASK_NAME_REF) {
247             // Name-only match, reuse the cached lookup instead of calling getIndex(name) again.
248             encodeLiteralWithNameRefStaticTable(out, index ^ QpackStaticTable.MASK_NAME_REF, value, true);
249         } else {
250             // Exact (name, value) match in the static table, an indexed static reference
251             // does not leak more information than what is already public, and the
252             // intermediary cannot gain any compression benefit by inserting a copy into
253             // its own dynamic table (the entry is already there as part of the static table).
254             encodeIndexedStaticTable(out, index);
255         }
256     }
257 
258     /**
259      * Encode the header field into the header block.
260      * @param qpackAttributes {@link QpackAttributes} for the channel.
261      * @param out {@link ByteBuf} to which encoded header field is to be written.
262      * @param base Base for the dynamic table index.
263      * @param name for the header field.
264      * @param value for the header field.
265      * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table,
266      * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } otherwise.
267      */
268     private int encodeHeader(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name,
269                              CharSequence value) {
270         int index = QpackStaticTable.findFieldIndex(name, value);
271         if (index == QpackStaticTable.NOT_FOUND) {
272             if (isDynamicTableUnavailable(qpackAttributes)) {
273                 encodeLiteral(out, name, value, false);
274                 return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
275             }
276             return encodeWithDynamicTable(qpackAttributes, out, base, name, value);
277         } else if ((index & QpackStaticTable.MASK_NAME_REF) == QpackStaticTable.MASK_NAME_REF) {
278             int dynamicTblIdx = tryEncodeWithDynamicTable(qpackAttributes, out, base, name, value);
279             if (dynamicTblIdx >= 0) {
280                 return dynamicTblIdx;
281             }
282             final int nameIdx = index ^ QpackStaticTable.MASK_NAME_REF;
283             dynamicTblIdx = tryAddToDynamicTable(qpackAttributes, true, nameIdx, name, value);
284             if (dynamicTblIdx >= 0) {
285                 if (dynamicTblIdx >= base) {
286                     encodePostBaseIndexed(out, base, dynamicTblIdx);
287                 } else {
288                     encodeIndexedDynamicTable(out, base, dynamicTblIdx);
289                 }
290                 return dynamicTblIdx;
291             }
292             encodeLiteralWithNameRefStaticTable(out, nameIdx, value, false);
293         } else {
294             encodeIndexedStaticTable(out, index);
295         }
296         return isDynamicTableUnavailable(qpackAttributes) ? DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE :
297                 DYNAMIC_TABLE_ENCODE_NOT_DONE;
298     }
299 
300     /**
301      * Encode the header field using dynamic table, if possible.
302      *
303      * @param qpackAttributes {@link QpackAttributes} for the channel.
304      * @param out {@link ByteBuf} to which encoded header field is to be written.
305      * @param base Base for the dynamic table index.
306      * @param name for the header field.
307      * @param value for the header field.
308      * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table,
309      * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } otherwise.
310      */
311     private int encodeWithDynamicTable(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name,
312                                        CharSequence value) {
313         int idx = tryEncodeWithDynamicTable(qpackAttributes, out, base, name, value);
314         if (idx >= 0) {
315             return idx;
316         }
317 
318         if (idx == DYNAMIC_TABLE_ENCODE_NOT_DONE) {
319             idx = tryAddToDynamicTable(qpackAttributes, false, -1, name, value);
320             if (idx >= 0) {
321                 if (idx >= base) {
322                     encodePostBaseIndexed(out, base, idx);
323                 } else {
324                     encodeIndexedDynamicTable(out, base, idx);
325                 }
326                 return idx;
327             }
328         }
329         encodeLiteral(out, name, value, false);
330         return idx;
331     }
332 
333     /**
334      * Try to encode the header field using dynamic table, otherwise do not encode.
335      *
336      * @param qpackAttributes {@link QpackAttributes} for the channel.
337      * @param out {@link ByteBuf} to which encoded header field is to be written.
338      * @param base Base for the dynamic table index.
339      * @param name for the header field.
340      * @param value for the header field.
341      * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table.
342      * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE } if encoding was not done. {@link #DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE }
343      * if dynamic table encoding is not possible (size constraint) and hence should not be tried for this header.
344      */
345     private int tryEncodeWithDynamicTable(QpackAttributes qpackAttributes, ByteBuf out, int base, CharSequence name,
346                                           CharSequence value) {
347         if (isDynamicTableUnavailable(qpackAttributes)) {
348             return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
349         }
350         assert qpackAttributes.encoderStreamAvailable();
351         final QuicStreamChannel encoderStream = qpackAttributes.encoderStream();
352 
353         int idx = dynamicTable.getEntryIndex(name, value);
354         if (idx == QpackEncoderDynamicTable.NOT_FOUND) {
355             return DYNAMIC_TABLE_ENCODE_NOT_DONE;
356         }
357         if (idx >= 0) {
358             if (dynamicTable.requiresDuplication(idx, sizeOf(name, value))) {
359                 idx = dynamicTable.add(name, value, sizeOf(name, value));
360                 assert idx >= 0;
361                 // https://www.rfc-editor.org/rfc/rfc9204.html#section-4.3.4
362                 //  0   1   2   3   4   5   6   7
363                 // +---+---+---+---+---+---+---+---+
364                 // | 0 | 0 | 0 |    Index (5+)     |
365                 // +---+---+---+-------------------+
366                 ByteBuf duplicate = encoderStream.alloc().buffer(8);
367                 encodePrefixedInteger(duplicate, (byte) 0b0000_0000, 5,
368                         dynamicTable.relativeIndexForEncoderInstructions(idx));
369                 closeOnFailure(encoderStream.writeAndFlush(duplicate));
370                 if (mayNotBlockStream()) {
371                     // Add to the table but do not use the entry in the header block to avoid blocking.
372                     return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
373                 }
374             }
375             if (idx >= base) {
376                 encodePostBaseIndexed(out, base, idx);
377             } else {
378                 encodeIndexedDynamicTable(out, base, idx);
379             }
380         } else { // name match
381             idx = -(idx + 1);
382             int addIdx = tryAddToDynamicTable(qpackAttributes, false,
383                     dynamicTable.relativeIndexForEncoderInstructions(idx), name, value);
384             if (addIdx < 0) {
385                 return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
386             }
387             idx = addIdx;
388 
389             if (idx >= base) {
390                 encodeLiteralWithPostBaseNameRef(out, base, idx, value);
391             } else {
392                 encodeLiteralWithNameRefDynamicTable(out, base, idx, value);
393             }
394         }
395         return idx;
396     }
397 
398     /**
399      * Try adding the header field to the dynamic table.
400      *
401      * @param qpackAttributes {@link QpackAttributes} for the channel.
402      * @param staticTableNameRef if {@code nameIdx} is an index in the static table.
403      * @param nameIdx Index of the name if {@code > 0}.
404      * @param name for the header field.
405      * @param value for the header field.
406      * @return Index in the dynamic table if the header field was encoded as a reference to the dynamic table,
407      * {@link #DYNAMIC_TABLE_ENCODE_NOT_DONE} otherwise.
408      */
409     private int tryAddToDynamicTable(QpackAttributes qpackAttributes, boolean staticTableNameRef, int nameIdx,
410                                      CharSequence name, CharSequence value) {
411         if (isDynamicTableUnavailable(qpackAttributes)) {
412             return DYNAMIC_TABLE_ENCODE_NOT_POSSIBLE;
413         }
414         assert qpackAttributes.encoderStreamAvailable();
415         final QuicStreamChannel encoderStream = qpackAttributes.encoderStream();
416 
417         int idx = dynamicTable.add(name, value, sizeOf(name, value));
418         if (idx >= 0) {
419             ByteBuf insert = null;
420             try {
421                 if (nameIdx >= 0) {
422                     // 2 prefixed integers (name index and value length) each requires a maximum of 8 bytes
423                     insert = encoderStream.alloc().buffer(value.length() + 16);
424                     // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-name-reference
425                     //    0   1   2   3   4   5   6   7
426                     // +---+---+---+---+---+---+---+---+
427                     // | 1 | T |    Name Index (6+)    |
428                     // +---+---+-----------------------+
429                     encodePrefixedInteger(insert, (byte) (staticTableNameRef ? 0b1100_0000 : 0b1000_0000), 6, nameIdx);
430                 } else {
431                     // 2 prefixed integers (name and value length) each requires a maximum of 8 bytes
432                     insert = encoderStream.alloc().buffer(name.length() + value.length() + 16);
433                     // https://www.rfc-editor.org/rfc/rfc9204.html#name-insert-with-literal-name
434                     //     0   1   2   3   4   5   6   7
435                     //   +---+---+---+---+---+---+---+---+
436                     //   | 0 | 1 | H | Name Length (5+)  |
437                     //   +---+---+---+-------------------+
438                     //   |  Name String (Length bytes)   |
439                     //   +---+---------------------------+
440                     // Names are always Huffman-encoded (H = 1). RFC 9204 makes
441                     // Huffman a pure size/CPU tradeoff and does not tie it to the
442                     // sensitivity of the field; whether intermediaries may index
443                     // the field is controlled separately by the N bit on the
444                     // matching literal field line.
445                     encodeLengthPrefixedHuffmanEncodedLiteral(insert, (byte) 0b0110_0000, 5, name);
446                 }
447                 //    0   1   2   3   4   5   6   7
448                 // +---+---+-----------------------+
449                 // | H |     Value Length (7+)     |
450                 // +---+---------------------------+
451                 // |  Value String (Length bytes)  |
452                 // +-------------------------------+
453                 encodeStringLiteral(insert, value);
454             } catch (Exception e) {
455                 ReferenceCountUtil.release(insert);
456                 return DYNAMIC_TABLE_ENCODE_NOT_DONE;
457             }
458             closeOnFailure(encoderStream.writeAndFlush(insert));
459             if (mayNotBlockStream()) {
460                 // Add to the table but do not use the entry in the header block to avoid blocking.
461                 return DYNAMIC_TABLE_ENCODE_NOT_DONE;
462             }
463             blockedStreams++;
464         }
465         return idx;
466     }
467 
468     private void encodeIndexedStaticTable(ByteBuf out, int index) {
469         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line
470         //   0   1   2   3   4   5   6   7
471         // +---+---+---+---+---+---+---+---+
472         // | 1 | T |      Index (6+)       |
473         // +---+---+-----------------------+
474         encodePrefixedInteger(out, (byte) 0b1100_0000, 6, index);
475     }
476 
477     private void encodeIndexedDynamicTable(ByteBuf out, int base, int index) {
478         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line
479         //   0   1   2   3   4   5   6   7
480         // +---+---+---+---+---+---+---+---+
481         // | 1 | T |      Index (6+)       |
482         // +---+---+-----------------------+
483         encodePrefixedInteger(out, (byte) 0b1000_0000, 6, base - index - 1);
484     }
485 
486     private void encodePostBaseIndexed(ByteBuf out, int base, int index) {
487         // https://www.rfc-editor.org/rfc/rfc9204.html#name-indexed-field-line-with-pos
488         //   0   1   2   3   4   5   6   7
489         // +---+---+---+---+---+---+---+---+
490         // | 0 | 0 | 0 | 1 |  Index (4+)   |
491         // +---+---+---+---+---------------+
492         encodePrefixedInteger(out, (byte) 0b0001_0000, 4, index - base);
493     }
494 
495     private void encodeLiteralWithNameRefStaticTable(ByteBuf out, int nameIndex, CharSequence value,
496                                                      boolean neverIndex) {
497         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam
498         //     0   1   2   3   4   5   6   7
499         //   +---+---+---+---+---+---+---+---+
500         //   | 0 | 1 | N | T |Name Index (4+)|
501         //   +---+---+---+---+---------------+
502         //   | H |     Value Length (7+)     |
503         //   +---+---------------------------+
504         //   |  Value String (Length bytes)  |
505         //   +-------------------------------+
506         //
507         // T = 1 (static table). N is driven by the sensitivity detector.
508         final byte prefix = (byte) (0b0101_0000 | (neverIndex ? 0b0010_0000 : 0));
509         encodePrefixedInteger(out, prefix, 4, nameIndex);
510         encodeStringLiteral(out, value);
511     }
512 
513     private void encodeLiteralWithNameRefDynamicTable(ByteBuf out, int base, int nameIndex, CharSequence value) {
514         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-nam
515         //     0   1   2   3   4   5   6   7
516         //   +---+---+---+---+---+---+---+---+
517         //   | 0 | 1 | N | T |Name Index (4+)|
518         //   +---+---+---+---+---------------+
519         //   | H |     Value Length (7+)     |
520         //   +---+---------------------------+
521         //   |  Value String (Length bytes)  |
522         //   +-------------------------------+
523         //
524         // T = 0 (dynamic table). Sensitive headers are routed through
525         // encodeSensitiveHeader() which bypasses the dynamic table entirely, so
526         // anything reaching this method is non-sensitive and N is always 0.
527         encodePrefixedInteger(out, (byte) 0b0100_0000, 4, base - nameIndex - 1);
528         encodeStringLiteral(out, value);
529     }
530 
531     private void encodeLiteralWithPostBaseNameRef(ByteBuf out, int base, int nameIndex, CharSequence value) {
532         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-pos
533         //    0   1   2   3   4   5   6   7
534         //   +---+---+---+---+---+---+---+---+
535         //   | 0 | 0 | 0 | 0 | N |NameIdx(3+)|
536         //   +---+---+---+---+---+-----------+
537         //   | H |     Value Length (7+)     |
538         //   +---+---------------------------+
539         //   |  Value String (Length bytes)  |
540         //   +-------------------------------+
541         //
542         // Same as above post-base references only exist for entries in the
543         // encoder's dynamic table, so sensitive headers never reach here and
544         // N is always 0.
545         encodePrefixedInteger(out, (byte) 0, 3, nameIndex - base);
546         encodeStringLiteral(out, value);
547     }
548 
549     private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value, boolean neverIndex) {
550         // https://www.rfc-editor.org/rfc/rfc9204.html#name-literal-field-line-with-lit
551         //   0   1   2   3   4   5   6   7
552         //   +---+---+---+---+---+---+---+---+
553         //   | 0 | 0 | 1 | N | H |NameLen(3+)|
554         //   +---+---+---+---+---+-----------+
555         //   |  Name String (Length bytes)   |
556         //   +---+---------------------------+
557         //   | H |     Value Length (7+)     |
558         //   +---+---------------------------+
559         //   |  Value String (Length bytes)  |
560         //   +-------------------------------+
561         //
562         // H = 1 (Huffman) is always set for the name length prefix.
563         final byte prefix = (byte) (0b0010_1000 | (neverIndex ? 0b0001_0000 : 0));
564         encodeLengthPrefixedHuffmanEncodedLiteral(out, prefix, 3, name);
565         encodeStringLiteral(out, value);
566     }
567 
568     /**
569      * Encode string literal according to Section 5.2.
570      * <a href="https://tools.ietf.org/html/rfc7541#section-5.2">Section 5.2</a>.
571      */
572     private void encodeStringLiteral(ByteBuf out, CharSequence value) {
573         //    0   1   2   3   4   5   6   7
574         // +---+---+---+---+---+---+---+---+
575         // | H |    String Length (7+)     |
576         // +---+---------------------------+
577         // |  String Data (Length octets)  |
578         // +-------------------------------+
579         // String values are always Huffman-encoded (H = 1). RFC 9204 treats the
580         // H bit as a pure size/CPU choice; whether intermediaries may index the
581         // field is controlled separately by the N bit on the literal field line.
582         encodeLengthPrefixedHuffmanEncodedLiteral(out, (byte) 0b1000_0000, 7, value);
583     }
584 
585     /**
586      * Encode a string literal.
587      */
588     private void encodeLengthPrefixedHuffmanEncodedLiteral(ByteBuf out, byte mask, int prefix, CharSequence value) {
589         int huffmanLength = huffmanEncoder.getEncodedLength(value);
590         encodePrefixedInteger(out, mask, prefix, huffmanLength);
591         huffmanEncoder.encode(out, value);
592     }
593 
594     private boolean mayNotBlockStream() {
595         return blockedStreams >= maxBlockedStreams - 1;
596     }
597 
598     /**
599      * Whether the dynamic table must not be used to encode the next header field: either because it was disabled
600      * for this connection, or because we are already tracking {@link #MAX_OUTSTANDING_SECTIONS} field sections
601      * that are awaiting a Section Acknowledgment or Stream Cancellation instruction from the peer.
602      *
603      * @param qpackAttributes  the attributes used.
604      * @return {@code true} if the dynamic table can't be used, {@code false} otherwise.
605      */
606     private boolean isDynamicTableUnavailable(QpackAttributes qpackAttributes) {
607         return qpackAttributes.dynamicTableDisabled() || outstandingSections >= MAX_OUTSTANDING_SECTIONS;
608     }
609 
610     // Visible for tests
611     int outstandingSectionCount() {
612         return outstandingSections;
613     }
614 
615     private static final class Indices {
616         private int idx;
617         // Let's just assume 4 indices for now that we will store here as max.
618         private int[] array = new int[4];
619 
620         void add(int index) {
621             if (idx == array.length) {
622                 // Double it if needed.
623                 array = Arrays.copyOf(array, array.length << 1);
624             }
625             array[idx++] = index;
626         }
627 
628         void forEach(IndexConsumer consumer) throws QpackException {
629             for (int i = 0; i < idx; i++) {
630                 consumer.accept(array[i]);
631             }
632         }
633 
634         @FunctionalInterface
635         interface IndexConsumer {
636             void accept(int idx) throws QpackException;
637         }
638     }
639 }