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 }