1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 import java.util.Objects;
39
40 import org.apache.hc.core5.annotation.Internal;
41 import org.apache.hc.core5.http.Header;
42 import org.apache.hc.core5.util.Args;
43 import org.apache.hc.core5.util.ByteArrayBuffer;
44
45
46
47
48
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 (Objects.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
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
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 }