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             dst.append(src);
114         }
115     }
116 
117     private void clearState() {
118 
119         if (this.tmpBuf != null) {
120             this.tmpBuf.clear();
121         }
122         if (this.charsetEncoder != null) {
123             this.charsetEncoder.reset();
124         }
125     }
126 
127     private void expandCapacity(final int capacity) {
128 
129         final ByteBuffer previous = this.tmpBuf;
130         this.tmpBuf = ByteBuffer.allocate(capacity);
131         previous.flip();
132         this.tmpBuf.put(previous);
133     }
134 
135     private void ensureCapacity(final int extra) {
136 
137         if (this.tmpBuf == null) {
138             this.tmpBuf = ByteBuffer.allocate(Math.max(256, extra));
139         }
140         final int requiredCapacity = this.tmpBuf.remaining() + extra;
141         if (requiredCapacity > this.tmpBuf.capacity()) {
142             expandCapacity(requiredCapacity);
143         }
144     }
145 
146     int encodeString(
147             final ByteArrayBuffer dst,
148             final CharSequence charSequence, final int off, final int len,
149             final boolean huffman) throws CharacterCodingException {
150 
151         clearState();
152         if (this.charsetEncoder == null) {
153             if (huffman) {
154                 this.huffmanBuf.clear();
155                 this.huffmanBuf.ensureCapacity(len);
156                 Huffman.ENCODER.encode(this.huffmanBuf, charSequence, off, len);
157                 dst.ensureCapacity(this.huffmanBuf.length() + 8);
158                 encodeInt(dst, 7, this.huffmanBuf.length(), 0x80);
159                 dst.append(this.huffmanBuf.array(), 0, this.huffmanBuf.length());
160             } else {
161                 dst.ensureCapacity(len + 8);
162                 encodeInt(dst, 7, len, 0x0);
163                 for (int i = 0; i < len; i++) {
164                     dst.append(charSequence.charAt(off + i));
165                 }
166             }
167             return len;
168         }
169         if (charSequence.length() > 0) {
170             final CharBuffer in = CharBuffer.wrap(charSequence, off, len);
171             while (in.hasRemaining()) {
172                 ensureCapacity((int) (in.remaining() * this.charsetEncoder.averageBytesPerChar()) + 8);
173                 final CoderResult result = this.charsetEncoder.encode(in, this.tmpBuf, true);
174                 if (result.isError()) {
175                     result.throwException();
176                 }
177             }
178             ensureCapacity(8);
179             final CoderResult result = this.charsetEncoder.flush(this.tmpBuf);
180             if (result.isError()) {
181                 result.throwException();
182             }
183         }
184         this.tmpBuf.flip();
185         final int binaryLen = this.tmpBuf.remaining();
186         encodeString(dst, this.tmpBuf, huffman);
187         return binaryLen;
188     }
189 
190     int encodeString(final ByteArrayBuffer dst, final String s, final boolean huffman) throws CharacterCodingException {
191 
192         return encodeString(dst, s, 0, s.length(), huffman);
193     }
194 
195     void encodeLiteralHeader(
196             final ByteArrayBuffer dst, final HPackEntry existing, final Header header,
197             final HPackRepresentation representation, final boolean useHuffman) throws CharacterCodingException {
198         encodeLiteralHeader(dst, existing, header.getName(), header.getValue(), header.isSensitive(), representation, useHuffman);
199     }
200 
201     void encodeLiteralHeader(
202             final ByteArrayBuffer dst, final HPackEntry existing, final String key, final String value, final boolean sensitive,
203             final HPackRepresentation representation, final boolean useHuffman) throws CharacterCodingException {
204 
205         final int n;
206         final int mask;
207         switch (representation) {
208             case WITH_INDEXING:
209                 mask = 0x40;
210                 n = 6;
211                 break;
212             case WITHOUT_INDEXING:
213                 mask = 0x00;
214                 n = 4;
215                 break;
216             case NEVER_INDEXED:
217                 mask = 0x10;
218                 n = 4;
219                 break;
220             default:
221                 throw new IllegalStateException("Unexpected value: " + representation);
222         }
223         final int index = existing != null ? existing.getIndex() : 0;
224         final int nameLen;
225         if (index <= 0) {
226             encodeInt(dst, n, 0, mask);
227             nameLen = encodeString(dst, key, useHuffman);
228         } else {
229             encodeInt(dst, n, index, mask);
230             nameLen = existing.getHeader().getNameLen();
231         }
232         final int valueLen = encodeString(dst, value != null ? value : "", useHuffman);
233         if (representation == HPackRepresentation.WITH_INDEXING) {
234             dynamicTable.add(new HPackHeader(key, nameLen, value, valueLen, sensitive));
235         }
236     }
237 
238     void encodeIndex(final ByteArrayBuffer dst, final int index) {
239         encodeInt(dst, 7, index, 0x80);
240     }
241 
242     private int findFullMatch(final List<HPackEntry> entries, final String value) {
243         if (entries == null || entries.isEmpty()) {
244             return 0;
245         }
246         for (int i = 0; i < entries.size(); i++) {
247             final HPackEntry entry = entries.get(i);
248             if (LangUtils.equals(value, entry.getHeader().getValue())) {
249                 return entry.getIndex();
250             }
251         }
252         return 0;
253     }
254 
255     void encodeHeader(
256             final ByteArrayBuffer dst, final Header header,
257             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
258         encodeHeader(dst, header.getName(), header.getValue(), header.isSensitive(), noIndexing, useHuffman);
259     }
260 
261     void encodeHeader(
262             final ByteArrayBuffer dst, final String name, final String value, final boolean sensitive,
263             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
264 
265         final HPackRepresentation representation;
266         if (sensitive) {
267             representation = HPackRepresentation.NEVER_INDEXED;
268         } else if (noIndexing) {
269             representation = HPackRepresentation.WITHOUT_INDEXING;
270         } else {
271             representation = HPackRepresentation.WITH_INDEXING;
272         }
273 
274         final List<HPackEntry> staticEntries = StaticTable.INSTANCE.getByName(name);
275 
276         if (representation == HPackRepresentation.WITH_INDEXING) {
277             // Try to find full match and encode as as index
278             final int staticIndex = findFullMatch(staticEntries, value);
279             if (staticIndex > 0) {
280                 encodeIndex(dst, staticIndex);
281                 return;
282             }
283             final List<HPackEntry> dynamicEntries = dynamicTable.getByName(name);
284             final int dynamicIndex = findFullMatch(dynamicEntries, value);
285             if (dynamicIndex > 0) {
286                 encodeIndex(dst, dynamicIndex);
287                 return;
288             }
289         }
290         // Encode as literal
291         HPackEntry existing = null;
292         if (staticEntries != null && !staticEntries.isEmpty()) {
293             existing = staticEntries.get(0);
294         } else {
295             final List<HPackEntry> dynamicEntries = dynamicTable.getByName(name);
296             if (dynamicEntries != null && !dynamicEntries.isEmpty()) {
297                 existing = dynamicEntries.get(0);
298             }
299         }
300         encodeLiteralHeader(dst, existing, name, value, sensitive, representation, useHuffman);
301     }
302 
303     void encodeHeaders(
304             final ByteArrayBuffer dst, final List<? extends Header> headers,
305             final boolean noIndexing, final boolean useHuffman) throws CharacterCodingException {
306         for (int i = 0; i < headers.size(); i++) {
307             encodeHeader(dst, headers.get(i), noIndexing, useHuffman);
308         }
309     }
310 
311     public void encodeHeader(
312             final ByteArrayBuffer dst, final Header header) throws CharacterCodingException {
313         Args.notNull(dst, "ByteArrayBuffer");
314         Args.notNull(header, "Header");
315         encodeHeader(dst, header.getName(), header.getValue(), header.isSensitive());
316     }
317 
318     public void encodeHeader(
319             final ByteArrayBuffer dst, final String name, final String value, final boolean sensitive) throws CharacterCodingException {
320         Args.notNull(dst, "ByteArrayBuffer");
321         Args.notEmpty(name, "Header name");
322         encodeHeader(dst, name, value, sensitive, false, true);
323     }
324 
325     public void encodeHeaders(
326             final ByteArrayBuffer dst, final List<? extends Header> headers, final boolean useHuffman) throws CharacterCodingException {
327         Args.notNull(dst, "ByteArrayBuffer");
328         Args.notEmpty(headers, "Header list");
329         encodeHeaders(dst, headers, false, useHuffman);
330     }
331 
332     public int getMaxTableSize() {
333         return this.maxTableSize;
334     }
335 
336     public void setMaxTableSize(final int maxTableSize) {
337         Args.notNegative(maxTableSize, "Max table size");
338         this.maxTableSize = maxTableSize;
339         this.dynamicTable.setMaxSize(maxTableSize);
340     }
341 
342 }