View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http2.hpack;
29  
30  import java.nio.ByteBuffer;
31  import java.nio.CharBuffer;
32  import java.nio.charset.CharacterCodingException;
33  import java.nio.charset.Charset;
34  import java.nio.charset.CharsetEncoder;
35  import java.nio.charset.CoderResult;
36  import java.nio.charset.StandardCharsets;
37  import java.util.List;
38  
39  import org.apache.hc.core5.annotation.Internal;
40  import org.apache.hc.core5.http.Header;
41  import org.apache.hc.core5.util.Args;
42  import org.apache.hc.core5.util.ByteArrayBuffer;
43  import org.apache.hc.core5.util.LangUtils;
44  
45  /**
46   * HPACK encoder.
47   *
48   * @since 5.0
49   */
50  @Internal
51  public final class HPackEncoder {
52  
53      private final OutboundDynamicTable dynamicTable;
54      private final ByteArrayBuffer huffmanBuf;
55      private final CharsetEncoder charsetEncoder;
56      private ByteBuffer tmpBuf;
57      private int maxTableSize;
58  
59      HPackEncoder(final OutboundDynamicTable dynamicTable, final CharsetEncoder charsetEncoder) {
60          this.dynamicTable = dynamicTable != null ? dynamicTable : new OutboundDynamicTable();
61          this.huffmanBuf = new ByteArrayBuffer(128);
62          this.charsetEncoder = charsetEncoder;
63      }
64  
65      HPackEncoder(final OutboundDynamicTable dynamicTable, final Charset charset) {
66          this(dynamicTable, charset != null && !StandardCharsets.US_ASCII.equals(charset) ? charset.newEncoder() : null);
67      }
68  
69      public HPackEncoder(final Charset charset) {
70          this(new OutboundDynamicTable(), charset);
71      }
72  
73      public HPackEncoder(final CharsetEncoder charsetEncoder) {
74          this(new OutboundDynamicTable(), charsetEncoder);
75      }
76  
77      static void encodeInt(final ByteArrayBuffer dst, final int n, final int i, final int mask) {
78  
79          final int nbits = 0xFF >>> (8 - n);
80          int value = i;
81          if (value < nbits) {
82              dst.append(i | mask);
83          } else {
84              dst.append(nbits | mask);
85              value -= nbits;
86  
87              while (value >= 0x80) {
88                  dst.append((value & 0x7F) | 0x80);
89                  value >>>= 7;
90              }
91              dst.append(value);
92          }
93      }
94  
95      static void encodeHuffman(final ByteArrayBuffer dst, final ByteBuffer src) {
96  
97          Huffman.ENCODER.encode(dst, src);
98      }
99  
100     void encodeString(final ByteArrayBuffer dst, final ByteBuffer src, final boolean huffman) {
101 
102         final int strLen = src.remaining();
103         if (huffman) {
104             this.huffmanBuf.clear();
105             this.huffmanBuf.ensureCapacity(strLen);
106             Huffman.ENCODER.encode(this.huffmanBuf, src);
107             dst.ensureCapacity(this.huffmanBuf.length() + 8);
108             encodeInt(dst, 7, this.huffmanBuf.length(), 0x80);
109             dst.append(this.huffmanBuf.array(), 0, this.huffmanBuf.length());
110         } else {
111             dst.ensureCapacity(strLen + 8);
112             encodeInt(dst, 7, strLen, 0x0);
113             if (src.hasArray()) {
114                 final byte[] b = src.array();
115                 final int off = src.position();
116                 dst.append(b, src.arrayOffset() + off, strLen);
117                 src.position(off + strLen);
118             } else {
119                 while (src.hasRemaining()) {
120                     dst.append(src.get());
121                 }
122             }
123         }
124     }
125 
126     private void clearState() {
127 
128         if (this.tmpBuf != null) {
129             this.tmpBuf.clear();
130         }
131         if (this.charsetEncoder != null) {
132             this.charsetEncoder.reset();
133         }
134     }
135 
136     private void expandCapacity(final int capacity) {
137 
138         final ByteBuffer previous = this.tmpBuf;
139         this.tmpBuf = ByteBuffer.allocate(capacity);
140         previous.flip();
141         this.tmpBuf.put(previous);
142     }
143 
144     private void ensureCapacity(final int extra) {
145 
146         if (this.tmpBuf == null) {
147             this.tmpBuf = ByteBuffer.allocate(Math.max(256, extra));
148         }
149         final int requiredCapacity = this.tmpBuf.remaining() + extra;
150         if (requiredCapacity > this.tmpBuf.capacity()) {
151             expandCapacity(requiredCapacity);
152         }
153     }
154 
155     int encodeString(
156             final ByteArrayBuffer dst,
157             final CharSequence charSequence, final int off, final int len,
158             final boolean huffman) throws CharacterCodingException {
159 
160         clearState();
161         if (this.charsetEncoder == null) {
162             if (huffman) {
163                 this.huffmanBuf.clear();
164                 this.huffmanBuf.ensureCapacity(len);
165                 Huffman.ENCODER.encode(this.huffmanBuf, charSequence, off, len);
166                 dst.ensureCapacity(this.huffmanBuf.length() + 8);
167                 encodeInt(dst, 7, this.huffmanBuf.length(), 0x80);
168                 dst.append(this.huffmanBuf.array(), 0, this.huffmanBuf.length());
169             } else {
170                 dst.ensureCapacity(len + 8);
171                 encodeInt(dst, 7, len, 0x0);
172                 for (int i = 0; i < len; i++) {
173                     dst.append(charSequence.charAt(off + i));
174                 }
175             }
176             return len;
177         }
178         final CharBuffer in = CharBuffer.wrap(charSequence, off, len);
179         while (in.hasRemaining()) {
180             ensureCapacity((int) (in.remaining() * this.charsetEncoder.averageBytesPerChar()) + 8);
181             final CoderResult result = this.charsetEncoder.encode(in, this.tmpBuf, true);
182             if (result.isError()) {
183                 result.throwException();
184             }
185         }
186         ensureCapacity(8);
187         final CoderResult result = this.charsetEncoder.flush(this.tmpBuf);
188         if (result.isError()) {
189             result.throwException();
190         }
191         this.tmpBuf.flip();
192         final int binaryLen = this.tmpBuf.remaining();
193         encodeString(dst, this.tmpBuf, huffman);
194         return binaryLen;
195     }
196 
197     int encodeString(final ByteArrayBuffer dst, final String s, final boolean huffman) throws CharacterCodingException {
198 
199         return encodeString(dst, s, 0, s.length(), huffman);
200     }
201 
202     void encodeLiteralHeader(
203             final ByteArrayBuffer dst, final HPackEntry existing, final Header header,
204             final HPackRepresentation representation, final boolean useHuffman) throws CharacterCodingException {
205         encodeLiteralHeader(dst, existing, header.getName(), header.getValue(), header.isSensitive(), representation, useHuffman);
206     }
207 
208     void encodeLiteralHeader(
209             final ByteArrayBuffer dst, final HPackEntry existing, final String key, final String value, final boolean sensitive,
210             final HPackRepresentation representation, final boolean useHuffman) throws CharacterCodingException {
211 
212         final int n;
213         final int mask;
214         switch (representation) {
215             case WITH_INDEXING:
216                 mask = 0x40;
217                 n = 6;
218                 break;
219             case WITHOUT_INDEXING:
220                 mask = 0x00;
221                 n = 4;
222                 break;
223             case NEVER_INDEXED:
224                 mask = 0x10;
225                 n = 4;
226                 break;
227             default:
228                 throw new IllegalStateException("Unexpected value: " + representation);
229         }
230         final int index = existing != null ? existing.getIndex() : 0;
231         final int nameLen;
232         if (index <= 0) {
233             encodeInt(dst, n, 0, mask);
234             nameLen = encodeString(dst, key, useHuffman);
235         } else {
236             encodeInt(dst, n, index, mask);
237             nameLen = existing.getHeader().getNameLen();
238         }
239         final int valueLen = encodeString(dst, value != null ? value : "", useHuffman);
240         if (representation == HPackRepresentation.WITH_INDEXING) {
241             dynamicTable.add(new HPackHeader(key, nameLen, value, valueLen, sensitive));
242         }
243     }
244 
245     void encodeIndex(final ByteArrayBuffer dst, final int index) {
246         encodeInt(dst, 7, index, 0x80);
247     }
248 
249     private int findFullMatch(final List<HPackEntry> entries, final String value) {
250         if (entries == null || entries.isEmpty()) {
251             return 0;
252         }
253         for (int i = 0; i < entries.size(); i++) {
254             final HPackEntry entry = entries.get(i);
255             if (LangUtils.equals(value, entry.getHeader().getValue())) {
256                 return entry.getIndex();
257             }
258         }
259         return 0;
260     }
261 
262     void encodeHeader(
263             final ByteArrayBuffer dst, final Header header,
264             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
265         encodeHeader(dst, header.getName(), header.getValue(), header.isSensitive(), noIndexing, useHuffman);
266     }
267 
268     void encodeHeader(
269             final ByteArrayBuffer dst, final String name, final String value, final boolean sensitive,
270             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
271 
272         final HPackRepresentation representation;
273         if (sensitive) {
274             representation = HPackRepresentation.NEVER_INDEXED;
275         } else if (noIndexing) {
276             representation = HPackRepresentation.WITHOUT_INDEXING;
277         } else {
278             representation = HPackRepresentation.WITH_INDEXING;
279         }
280 
281         final List<HPackEntry> staticEntries = StaticTable.INSTANCE.getByName(name);
282 
283         if (representation == HPackRepresentation.WITH_INDEXING) {
284             // Try to find full match and encode as as index
285             final int staticIndex = findFullMatch(staticEntries, value);
286             if (staticIndex > 0) {
287                 encodeIndex(dst, staticIndex);
288                 return;
289             }
290             final List<HPackEntry> dynamicEntries = dynamicTable.getByName(name);
291             final int dynamicIndex = findFullMatch(dynamicEntries, value);
292             if (dynamicIndex > 0) {
293                 encodeIndex(dst, dynamicIndex);
294                 return;
295             }
296         }
297         // Encode as literal
298         HPackEntry existing = null;
299         if (staticEntries != null && !staticEntries.isEmpty()) {
300             existing = staticEntries.get(0);
301         } else {
302             final List<HPackEntry> dynamicEntries = dynamicTable.getByName(name);
303             if (dynamicEntries != null && !dynamicEntries.isEmpty()) {
304                 existing = dynamicEntries.get(0);
305             }
306         }
307         encodeLiteralHeader(dst, existing, name, value, sensitive, representation, useHuffman);
308     }
309 
310     void encodeHeaders(
311             final ByteArrayBuffer dst, final List<? extends Header> headers,
312             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
313         for (int i = 0; i < headers.size(); i++) {
314             encodeHeader(dst, headers.get(i), noIndexing, useHuffman);
315         }
316     }
317 
318     public void encodeHeader(
319             final ByteArrayBuffer dst, final Header header) throws CharacterCodingException {
320         Args.notNull(dst, "ByteArrayBuffer");
321         Args.notNull(header, "Header");
322         encodeHeader(dst, header.getName(), header.getValue(), header.isSensitive());
323     }
324 
325     public void encodeHeader(
326             final ByteArrayBuffer dst, final String name, final String value, final boolean sensitive) throws CharacterCodingException {
327         Args.notNull(dst, "ByteArrayBuffer");
328         Args.notEmpty(name, "Header name");
329         encodeHeader(dst, name, value, sensitive, false, true);
330     }
331 
332     public void encodeHeaders(
333             final ByteArrayBuffer dst, final List<? extends Header> headers, final boolean useHuffman) throws CharacterCodingException {
334         Args.notNull(dst, "ByteArrayBuffer");
335         Args.notEmpty(headers, "Header list");
336         encodeHeaders(dst, headers, false, useHuffman);
337     }
338 
339     public int getMaxTableSize() {
340         return this.maxTableSize;
341     }
342 
343     public void setMaxTableSize(final int maxTableSize) {
344         Args.notNegative(maxTableSize, "Max table size");
345         this.maxTableSize = maxTableSize;
346         this.dynamicTable.setMaxSize(maxTableSize);
347     }
348 
349 }