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 jdk.incubator.http.internal.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 }