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
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
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 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
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
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 }