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