1 /* |
|
2 * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 package sun.net.httpclient.hpack; |
|
26 |
|
27 import java.nio.ByteBuffer; |
|
28 import java.nio.ReadOnlyBufferException; |
|
29 import java.util.LinkedList; |
|
30 import java.util.List; |
|
31 |
|
32 import static java.lang.String.format; |
|
33 import static java.util.Objects.requireNonNull; |
|
34 |
|
35 /** |
|
36 * Encodes headers to their binary representation. |
|
37 * |
|
38 * <p>Typical lifecycle looks like this: |
|
39 * |
|
40 * <p> {@link #Encoder(int) new Encoder} |
|
41 * ({@link #setMaxCapacity(int) setMaxCapacity}? |
|
42 * {@link #encode(ByteBuffer) encode})* |
|
43 * |
|
44 * <p> Suppose headers are represented by {@code Map<String, List<String>>}. A |
|
45 * supplier and a consumer of {@link ByteBuffer}s in forms of {@code |
|
46 * Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively. Then to |
|
47 * encode headers, the following approach might be used: |
|
48 * |
|
49 * <pre>{@code |
|
50 * for (Map.Entry<String, List<String>> h : headers.entrySet()) { |
|
51 * String name = h.getKey(); |
|
52 * for (String value : h.getValue()) { |
|
53 * encoder.header(name, value); // Set up header |
|
54 * boolean encoded; |
|
55 * do { |
|
56 * ByteBuffer b = buffersSupplier.get(); |
|
57 * encoded = encoder.encode(b); // Encode the header |
|
58 * buffersConsumer.accept(b); |
|
59 * } while (!encoded); |
|
60 * } |
|
61 * } |
|
62 * }</pre> |
|
63 * |
|
64 * <p> Though the specification <a |
|
65 * href="https://tools.ietf.org/html/rfc7541#section-2"> does not define</a> how |
|
66 * an encoder is to be implemented, a default implementation is provided by the |
|
67 * method {@link #header(CharSequence, CharSequence, boolean)}. |
|
68 * |
|
69 * <p> To provide a custom encoding implementation, {@code Encoder} has to be |
|
70 * extended. A subclass then can access methods for encoding using specific |
|
71 * representations (e.g. {@link #literal(int, CharSequence, boolean) literal}, |
|
72 * {@link #indexed(int) indexed}, etc.) |
|
73 * |
|
74 * @apiNote |
|
75 * |
|
76 * <p> An Encoder provides an incremental way of encoding headers. |
|
77 * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating |
|
78 * whether, or not, the buffer was sufficiently sized to hold the |
|
79 * remaining of the encoded representation. |
|
80 * |
|
81 * <p> This way, there's no need to provide a buffer of a specific size, or to |
|
82 * resize (and copy) the buffer on demand, when the remaining encoded |
|
83 * representation will not fit in the buffer's remaining space. Instead, an |
|
84 * array of existing buffers can be used, prepended with a frame that encloses |
|
85 * the resulting header block afterwards. |
|
86 * |
|
87 * <p> Splitting the encoding operation into header set up and header encoding, |
|
88 * separates long lived arguments ({@code name}, {@code value}, {@code |
|
89 * sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}), |
|
90 * simplifying each operation itself. |
|
91 * |
|
92 * @implNote |
|
93 * |
|
94 * <p> The default implementation does not use dynamic table. It reports to a |
|
95 * coupled Decoder a size update with the value of {@code 0}, and never changes |
|
96 * it afterwards. |
|
97 * |
|
98 * @since 9 |
|
99 */ |
|
100 public class Encoder { |
|
101 |
|
102 // TODO: enum: no huffman/smart huffman/always huffman |
|
103 private static final boolean DEFAULT_HUFFMAN = true; |
|
104 |
|
105 private final IndexedWriter indexedWriter = new IndexedWriter(); |
|
106 private final LiteralWriter literalWriter = new LiteralWriter(); |
|
107 private final LiteralNeverIndexedWriter literalNeverIndexedWriter |
|
108 = new LiteralNeverIndexedWriter(); |
|
109 private final LiteralWithIndexingWriter literalWithIndexingWriter |
|
110 = new LiteralWithIndexingWriter(); |
|
111 private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter(); |
|
112 private final BulkSizeUpdateWriter bulkSizeUpdateWriter |
|
113 = new BulkSizeUpdateWriter(); |
|
114 |
|
115 private BinaryRepresentationWriter writer; |
|
116 private final HeaderTable headerTable; |
|
117 |
|
118 private boolean encoding; |
|
119 |
|
120 private int maxCapacity; |
|
121 private int currCapacity; |
|
122 private int lastCapacity; |
|
123 private long minCapacity; |
|
124 private boolean capacityUpdate; |
|
125 private boolean configuredCapacityUpdate; |
|
126 |
|
127 /** |
|
128 * Constructs an {@code Encoder} with the specified maximum capacity of the |
|
129 * header table. |
|
130 * |
|
131 * <p> The value has to be agreed between decoder and encoder out-of-band, |
|
132 * e.g. by a protocol that uses HPACK (see <a |
|
133 * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table |
|
134 * Size</a>). |
|
135 * |
|
136 * @param maxCapacity |
|
137 * a non-negative integer |
|
138 * |
|
139 * @throws IllegalArgumentException |
|
140 * if maxCapacity is negative |
|
141 */ |
|
142 public Encoder(int maxCapacity) { |
|
143 if (maxCapacity < 0) { |
|
144 throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity); |
|
145 } |
|
146 // Initial maximum capacity update mechanics |
|
147 minCapacity = Long.MAX_VALUE; |
|
148 currCapacity = -1; |
|
149 setMaxCapacity(maxCapacity); |
|
150 headerTable = new HeaderTable(lastCapacity); |
|
151 } |
|
152 |
|
153 /** |
|
154 * Sets up the given header {@code (name, value)}. |
|
155 * |
|
156 * <p> Fixates {@code name} and {@code value} for the duration of encoding. |
|
157 * |
|
158 * @param name |
|
159 * the name |
|
160 * @param value |
|
161 * the value |
|
162 * |
|
163 * @throws NullPointerException |
|
164 * if any of the arguments are {@code null} |
|
165 * @throws IllegalStateException |
|
166 * if the encoder hasn't fully encoded the previous header, or |
|
167 * hasn't yet started to encode it |
|
168 * @see #header(CharSequence, CharSequence, boolean) |
|
169 */ |
|
170 public void header(CharSequence name, CharSequence value) |
|
171 throws IllegalStateException { |
|
172 header(name, value, false); |
|
173 } |
|
174 |
|
175 /** |
|
176 * Sets up the given header {@code (name, value)} with possibly sensitive |
|
177 * value. |
|
178 * |
|
179 * <p> Fixates {@code name} and {@code value} for the duration of encoding. |
|
180 * |
|
181 * @param name |
|
182 * the name |
|
183 * @param value |
|
184 * the value |
|
185 * @param sensitive |
|
186 * whether or not the value is sensitive |
|
187 * |
|
188 * @throws NullPointerException |
|
189 * if any of the arguments are {@code null} |
|
190 * @throws IllegalStateException |
|
191 * if the encoder hasn't fully encoded the previous header, or |
|
192 * hasn't yet started to encode it |
|
193 * @see #header(CharSequence, CharSequence) |
|
194 * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) |
|
195 */ |
|
196 public void header(CharSequence name, CharSequence value, |
|
197 boolean sensitive) throws IllegalStateException { |
|
198 // Arguably a good balance between complexity of implementation and |
|
199 // efficiency of encoding |
|
200 requireNonNull(name, "name"); |
|
201 requireNonNull(value, "value"); |
|
202 HeaderTable t = getHeaderTable(); |
|
203 int index = t.indexOf(name, value); |
|
204 if (index > 0) { |
|
205 indexed(index); |
|
206 } else if (index < 0) { |
|
207 if (sensitive) { |
|
208 literalNeverIndexed(-index, value, DEFAULT_HUFFMAN); |
|
209 } else { |
|
210 literal(-index, value, DEFAULT_HUFFMAN); |
|
211 } |
|
212 } else { |
|
213 if (sensitive) { |
|
214 literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); |
|
215 } else { |
|
216 literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); |
|
217 } |
|
218 } |
|
219 } |
|
220 |
|
221 /** |
|
222 * Sets a maximum capacity of the header table. |
|
223 * |
|
224 * <p> The value has to be agreed between decoder and encoder out-of-band, |
|
225 * e.g. by a protocol that uses HPACK (see <a |
|
226 * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table |
|
227 * Size</a>). |
|
228 * |
|
229 * <p> May be called any number of times after or before a complete header |
|
230 * has been encoded. |
|
231 * |
|
232 * <p> If the encoder decides to change the actual capacity, an update will |
|
233 * be encoded before a new encoding operation starts. |
|
234 * |
|
235 * @param capacity |
|
236 * a non-negative integer |
|
237 * |
|
238 * @throws IllegalArgumentException |
|
239 * if capacity is negative |
|
240 * @throws IllegalStateException |
|
241 * if the encoder hasn't fully encoded the previous header, or |
|
242 * hasn't yet started to encode it |
|
243 */ |
|
244 public void setMaxCapacity(int capacity) { |
|
245 checkEncoding(); |
|
246 if (capacity < 0) { |
|
247 throw new IllegalArgumentException("capacity >= 0: " + capacity); |
|
248 } |
|
249 int calculated = calculateCapacity(capacity); |
|
250 if (calculated < 0 || calculated > capacity) { |
|
251 throw new IllegalArgumentException( |
|
252 format("0 <= calculated <= capacity: calculated=%s, capacity=%s", |
|
253 calculated, capacity)); |
|
254 } |
|
255 capacityUpdate = true; |
|
256 // maxCapacity needs to be updated unconditionally, so the encoder |
|
257 // always has the newest one (in case it decides to update it later |
|
258 // unsolicitedly) |
|
259 // Suppose maxCapacity = 4096, and the encoder has decided to use only |
|
260 // 2048. It later can choose anything else from the region [0, 4096]. |
|
261 maxCapacity = capacity; |
|
262 lastCapacity = calculated; |
|
263 minCapacity = Math.min(minCapacity, lastCapacity); |
|
264 } |
|
265 |
|
266 protected int calculateCapacity(int maxCapacity) { |
|
267 // Default implementation of the Encoder won't add anything to the |
|
268 // table, therefore no need for a table space |
|
269 return 0; |
|
270 } |
|
271 |
|
272 /** |
|
273 * Encodes the {@linkplain #header(CharSequence, CharSequence) set up} |
|
274 * header into the given buffer. |
|
275 * |
|
276 * <p> The encoder writes as much as possible of the header's binary |
|
277 * representation into the given buffer, starting at the buffer's position, |
|
278 * and increments its position to reflect the bytes written. The buffer's |
|
279 * mark and limit will not be modified. |
|
280 * |
|
281 * <p> Once the method has returned {@code true}, the current header is |
|
282 * deemed encoded. A new header may be set up. |
|
283 * |
|
284 * @param headerBlock |
|
285 * the buffer to encode the header into, may be empty |
|
286 * |
|
287 * @return {@code true} if the current header has been fully encoded, |
|
288 * {@code false} otherwise |
|
289 * |
|
290 * @throws NullPointerException |
|
291 * if the buffer is {@code null} |
|
292 * @throws ReadOnlyBufferException |
|
293 * if this buffer is read-only |
|
294 * @throws IllegalStateException |
|
295 * if there is no set up header |
|
296 */ |
|
297 public final boolean encode(ByteBuffer headerBlock) { |
|
298 if (!encoding) { |
|
299 throw new IllegalStateException("A header hasn't been set up"); |
|
300 } |
|
301 if (!prependWithCapacityUpdate(headerBlock)) { |
|
302 return false; |
|
303 } |
|
304 boolean done = writer.write(headerTable, headerBlock); |
|
305 if (done) { |
|
306 writer.reset(); // FIXME: WHY? |
|
307 encoding = false; |
|
308 } |
|
309 return done; |
|
310 } |
|
311 |
|
312 private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) { |
|
313 if (capacityUpdate) { |
|
314 if (!configuredCapacityUpdate) { |
|
315 List<Integer> sizes = new LinkedList<>(); |
|
316 if (minCapacity < currCapacity) { |
|
317 sizes.add((int) minCapacity); |
|
318 if (minCapacity != lastCapacity) { |
|
319 sizes.add(lastCapacity); |
|
320 } |
|
321 } else if (lastCapacity != currCapacity) { |
|
322 sizes.add(lastCapacity); |
|
323 } |
|
324 bulkSizeUpdateWriter.maxHeaderTableSizes(sizes); |
|
325 configuredCapacityUpdate = true; |
|
326 } |
|
327 boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock); |
|
328 if (done) { |
|
329 minCapacity = lastCapacity; |
|
330 currCapacity = lastCapacity; |
|
331 bulkSizeUpdateWriter.reset(); |
|
332 capacityUpdate = false; |
|
333 configuredCapacityUpdate = false; |
|
334 } |
|
335 return done; |
|
336 } |
|
337 return true; |
|
338 } |
|
339 |
|
340 protected final void indexed(int index) throws IndexOutOfBoundsException { |
|
341 checkEncoding(); |
|
342 encoding = true; |
|
343 writer = indexedWriter.index(index); |
|
344 } |
|
345 |
|
346 protected final void literal(int index, CharSequence value, |
|
347 boolean useHuffman) |
|
348 throws IndexOutOfBoundsException { |
|
349 checkEncoding(); |
|
350 encoding = true; |
|
351 writer = literalWriter |
|
352 .index(index).value(value, useHuffman); |
|
353 } |
|
354 |
|
355 protected final void literal(CharSequence name, boolean nameHuffman, |
|
356 CharSequence value, boolean valueHuffman) { |
|
357 checkEncoding(); |
|
358 encoding = true; |
|
359 writer = literalWriter |
|
360 .name(name, nameHuffman).value(value, valueHuffman); |
|
361 } |
|
362 |
|
363 protected final void literalNeverIndexed(int index, |
|
364 CharSequence value, |
|
365 boolean valueHuffman) |
|
366 throws IndexOutOfBoundsException { |
|
367 checkEncoding(); |
|
368 encoding = true; |
|
369 writer = literalNeverIndexedWriter |
|
370 .index(index).value(value, valueHuffman); |
|
371 } |
|
372 |
|
373 protected final void literalNeverIndexed(CharSequence name, |
|
374 boolean nameHuffman, |
|
375 CharSequence value, |
|
376 boolean valueHuffman) { |
|
377 checkEncoding(); |
|
378 encoding = true; |
|
379 writer = literalNeverIndexedWriter |
|
380 .name(name, nameHuffman).value(value, valueHuffman); |
|
381 } |
|
382 |
|
383 protected final void literalWithIndexing(int index, |
|
384 CharSequence value, |
|
385 boolean valueHuffman) |
|
386 throws IndexOutOfBoundsException { |
|
387 checkEncoding(); |
|
388 encoding = true; |
|
389 writer = literalWithIndexingWriter |
|
390 .index(index).value(value, valueHuffman); |
|
391 } |
|
392 |
|
393 protected final void literalWithIndexing(CharSequence name, |
|
394 boolean nameHuffman, |
|
395 CharSequence value, |
|
396 boolean valueHuffman) { |
|
397 checkEncoding(); |
|
398 encoding = true; |
|
399 writer = literalWithIndexingWriter |
|
400 .name(name, nameHuffman).value(value, valueHuffman); |
|
401 } |
|
402 |
|
403 protected final void sizeUpdate(int capacity) |
|
404 throws IllegalArgumentException { |
|
405 checkEncoding(); |
|
406 // Ensure subclass follows the contract |
|
407 if (capacity > this.maxCapacity) { |
|
408 throw new IllegalArgumentException( |
|
409 format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s", |
|
410 capacity, maxCapacity)); |
|
411 } |
|
412 writer = sizeUpdateWriter.maxHeaderTableSize(capacity); |
|
413 } |
|
414 |
|
415 protected final int getMaxCapacity() { |
|
416 return maxCapacity; |
|
417 } |
|
418 |
|
419 protected final HeaderTable getHeaderTable() { |
|
420 return headerTable; |
|
421 } |
|
422 |
|
423 protected final void checkEncoding() { |
|
424 if (encoding) { |
|
425 throw new IllegalStateException( |
|
426 "Previous encoding operation hasn't finished yet"); |
|
427 } |
|
428 } |
|
429 } |
|