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