1 /*
   2  * Copyright (c) 2014, 2017, 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 jdk.incubator.http.internal.hpack.HPACK.Logger;
  28 import jdk.internal.vm.annotation.Stable;
  29 
  30 import java.util.HashMap;
  31 import java.util.LinkedHashMap;
  32 import java.util.Map;
  33 import java.util.NoSuchElementException;
  34 
  35 import static java.lang.String.format;
  36 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
  37 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
  38 
  39 //
  40 // Header Table combined from two tables: static and dynamic.
  41 //
  42 // There is a single address space for index values. Index-aware methods
  43 // correspond to the table as a whole. Size-aware methods only to the dynamic
  44 // part of it.
  45 //
  46 final class HeaderTable {
  47 
  48     @Stable
  49     private static final HeaderField[] staticTable = {
  50             null, // To make index 1-based, instead of 0-based
  51             new HeaderField(":authority"),
  52             new HeaderField(":method", "GET"),
  53             new HeaderField(":method", "POST"),
  54             new HeaderField(":path", "/"),
  55             new HeaderField(":path", "/index.html"),
  56             new HeaderField(":scheme", "http"),
  57             new HeaderField(":scheme", "https"),
  58             new HeaderField(":status", "200"),
  59             new HeaderField(":status", "204"),
  60             new HeaderField(":status", "206"),
  61             new HeaderField(":status", "304"),
  62             new HeaderField(":status", "400"),
  63             new HeaderField(":status", "404"),
  64             new HeaderField(":status", "500"),
  65             new HeaderField("accept-charset"),
  66             new HeaderField("accept-encoding", "gzip, deflate"),
  67             new HeaderField("accept-language"),
  68             new HeaderField("accept-ranges"),
  69             new HeaderField("accept"),
  70             new HeaderField("access-control-allow-origin"),
  71             new HeaderField("age"),
  72             new HeaderField("allow"),
  73             new HeaderField("authorization"),
  74             new HeaderField("cache-control"),
  75             new HeaderField("content-disposition"),
  76             new HeaderField("content-encoding"),
  77             new HeaderField("content-language"),
  78             new HeaderField("content-length"),
  79             new HeaderField("content-location"),
  80             new HeaderField("content-range"),
  81             new HeaderField("content-type"),
  82             new HeaderField("cookie"),
  83             new HeaderField("date"),
  84             new HeaderField("etag"),
  85             new HeaderField("expect"),
  86             new HeaderField("expires"),
  87             new HeaderField("from"),
  88             new HeaderField("host"),
  89             new HeaderField("if-match"),
  90             new HeaderField("if-modified-since"),
  91             new HeaderField("if-none-match"),
  92             new HeaderField("if-range"),
  93             new HeaderField("if-unmodified-since"),
  94             new HeaderField("last-modified"),
  95             new HeaderField("link"),
  96             new HeaderField("location"),
  97             new HeaderField("max-forwards"),
  98             new HeaderField("proxy-authenticate"),
  99             new HeaderField("proxy-authorization"),
 100             new HeaderField("range"),
 101             new HeaderField("referer"),
 102             new HeaderField("refresh"),
 103             new HeaderField("retry-after"),
 104             new HeaderField("server"),
 105             new HeaderField("set-cookie"),
 106             new HeaderField("strict-transport-security"),
 107             new HeaderField("transfer-encoding"),
 108             new HeaderField("user-agent"),
 109             new HeaderField("vary"),
 110             new HeaderField("via"),
 111             new HeaderField("www-authenticate")
 112     };
 113 
 114     private static final int STATIC_TABLE_LENGTH = staticTable.length - 1;
 115     private static final int ENTRY_SIZE = 32;
 116     private static final Map<String, LinkedHashMap<String, Integer>> staticIndexes;
 117 
 118     static {
 119         staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); // TODO: Map.of
 120         for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) {
 121             HeaderField f = staticTable[i];
 122             Map<String, Integer> values = staticIndexes
 123                     .computeIfAbsent(f.name, k -> new LinkedHashMap<>());
 124             values.put(f.value, i);
 125         }
 126     }
 127 
 128     private final Logger logger;
 129     private final Table dynamicTable = new Table(0);
 130     private int maxSize;
 131     private int size;
 132 
 133     public HeaderTable(int maxSize, Logger logger) {
 134         this.logger = logger;
 135         setMaxSize(maxSize);
 136     }
 137 
 138     //
 139     // The method returns:
 140     //
 141     // * a positive integer i where i (i = [1..Integer.MAX_VALUE]) is an
 142     // index of an entry with a header (n, v), where n.equals(name) &&
 143     // v.equals(value)
 144     //
 145     // * a negative integer j where j (j = [-Integer.MAX_VALUE..-1]) is an
 146     // index of an entry with a header (n, v), where n.equals(name)
 147     //
 148     // * 0 if there's no entry e such that e.getName().equals(name)
 149     //
 150     // The rationale behind this design is to allow to pack more useful data
 151     // into a single invocation, facilitating a single pass where possible
 152     // (the idea is the same as in java.util.Arrays.binarySearch(int[], int)).
 153     //
 154     public int indexOf(CharSequence name, CharSequence value) {
 155         // Invoking toString() will possibly allocate Strings for the sake of
 156         // the search, which doesn't feel right.
 157         String n = name.toString();
 158         String v = value.toString();
 159 
 160         // 1. Try exact match in the static region
 161         Map<String, Integer> values = staticIndexes.get(n);
 162         if (values != null) {
 163             Integer idx = values.get(v);
 164             if (idx != null) {
 165                 return idx;
 166             }
 167         }
 168         // 2. Try exact match in the dynamic region
 169         int didx = dynamicTable.indexOf(n, v);
 170         if (didx > 0) {
 171             return STATIC_TABLE_LENGTH + didx;
 172         } else if (didx < 0) {
 173             if (values != null) {
 174                 // 3. Return name match from the static region
 175                 return -values.values().iterator().next(); // Iterator allocation
 176             } else {
 177                 // 4. Return name match from the dynamic region
 178                 return -STATIC_TABLE_LENGTH + didx;
 179             }
 180         } else {
 181             if (values != null) {
 182                 // 3. Return name match from the static region
 183                 return -values.values().iterator().next(); // Iterator allocation
 184             } else {
 185                 return 0;
 186             }
 187         }
 188     }
 189 
 190     public int size() {
 191         return size;
 192     }
 193 
 194     public int maxSize() {
 195         return maxSize;
 196     }
 197 
 198     public int length() {
 199         return STATIC_TABLE_LENGTH + dynamicTable.size();
 200     }
 201 
 202     HeaderField get(int index) {
 203         checkIndex(index);
 204         if (index <= STATIC_TABLE_LENGTH) {
 205             return staticTable[index];
 206         } else {
 207             return dynamicTable.get(index - STATIC_TABLE_LENGTH);
 208         }
 209     }
 210 
 211     void put(CharSequence name, CharSequence value) {
 212         // Invoking toString() will possibly allocate Strings. But that's
 213         // unavoidable at this stage. If a CharSequence is going to be stored in
 214         // the table, it must not be mutable (e.g. for the sake of hashing).
 215         put(new HeaderField(name.toString(), value.toString()));
 216     }
 217 
 218     private void put(HeaderField h) {
 219         if (logger.isLoggable(NORMAL)) {
 220             logger.log(NORMAL, () -> format("adding ('%s', '%s')",
 221                                             h.name, h.value));
 222         }
 223         int entrySize = sizeOf(h);
 224         if (logger.isLoggable(EXTRA)) {
 225             logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s",
 226                                            h.name, h.value, entrySize));
 227         }
 228         while (entrySize > maxSize - size && size != 0) {
 229             if (logger.isLoggable(EXTRA)) {
 230                 logger.log(EXTRA, () -> format("insufficient space %s, must evict entry",
 231                                                (maxSize - size)));
 232             }
 233             evictEntry();
 234         }
 235         if (entrySize > maxSize - size) {
 236             if (logger.isLoggable(EXTRA)) {
 237                 logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big",
 238                                                h.name, h.value));
 239             }
 240             return;
 241         }
 242         size += entrySize;
 243         dynamicTable.add(h);
 244         if (logger.isLoggable(EXTRA)) {
 245             logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value));
 246             logger.log(EXTRA, this::toString);
 247         }
 248     }
 249 
 250     void setMaxSize(int maxSize) {
 251         if (maxSize < 0) {
 252             throw new IllegalArgumentException(
 253                     "maxSize >= 0: maxSize=" + maxSize);
 254         }
 255         while (maxSize < size && size != 0) {
 256             evictEntry();
 257         }
 258         this.maxSize = maxSize;
 259         int upperBound = (maxSize / ENTRY_SIZE) + 1;
 260         this.dynamicTable.setCapacity(upperBound);
 261     }
 262 
 263     HeaderField evictEntry() {
 264         HeaderField f = dynamicTable.remove();
 265         int s = sizeOf(f);
 266         this.size -= s;
 267         if (logger.isLoggable(EXTRA)) {
 268             logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s",
 269                                            f.name, f.value, s));
 270             logger.log(EXTRA, this::toString);
 271         }
 272         return f;
 273     }
 274 
 275     @Override
 276     public String toString() {
 277         double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize);
 278         return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)",
 279                       dynamicTable.size(), length(), size, maxSize, used);
 280     }
 281 
 282     private int checkIndex(int index) {
 283         int len = length();
 284         if (index < 1 || index > len) {
 285             throw new IndexOutOfBoundsException(
 286                     format("1 <= index <= length(): index=%s, length()=%s",
 287                            index, len));
 288         }
 289         return index;
 290     }
 291 
 292     int sizeOf(HeaderField f) {
 293         return f.name.length() + f.value.length() + ENTRY_SIZE;
 294     }
 295 
 296     //
 297     // Diagnostic information in the form used in the RFC 7541
 298     //
 299     String getStateString() {
 300         if (size == 0) {
 301             return "empty.";
 302         }
 303 
 304         StringBuilder b = new StringBuilder();
 305         for (int i = 1, size = dynamicTable.size(); i <= size; i++) {
 306             HeaderField e = dynamicTable.get(i);
 307             b.append(format("[%3d] (s = %3d) %s: %s\n", i,
 308                     sizeOf(e), e.name, e.value));
 309         }
 310         b.append(format("      Table size:%4s", this.size));
 311         return b.toString();
 312     }
 313 
 314     // Convert to a Value Object (JDK-8046159)?
 315     static final class HeaderField {
 316 
 317         final String name;
 318         final String value;
 319 
 320         public HeaderField(String name) {
 321             this(name, "");
 322         }
 323 
 324         public HeaderField(String name, String value) {
 325             this.name = name;
 326             this.value = value;
 327         }
 328 
 329         @Override
 330         public String toString() {
 331             return value.isEmpty() ? name : name + ": " + value;
 332         }
 333 
 334         @Override
 335         public boolean equals(Object o) {
 336             if (this == o) {
 337                 return true;
 338             }
 339             if (o == null || getClass() != o.getClass()) {
 340                 return false;
 341             }
 342             HeaderField that = (HeaderField) o;
 343             return name.equals(that.name) && value.equals(that.value);
 344         }
 345 
 346         @Override
 347         public int hashCode() {
 348             return 31 * name.hashCode() + value.hashCode();
 349         }
 350     }
 351 
 352     //
 353     // To quickly find an index of an entry in the dynamic table with the given
 354     // contents an effective inverse mapping is needed. Here's a simple idea
 355     // behind such a mapping.
 356     //
 357     // # The problem:
 358     //
 359     // We have a queue with an O(1) lookup by index:
 360     //
 361     //     get: index -> x
 362     //
 363     // What we want is an O(1) reverse lookup:
 364     //
 365     //     indexOf: x -> index
 366     //
 367     // # Solution:
 368     //
 369     // Let's store an inverse mapping in a Map<x, Integer>. This have a problem
 370     // that when a new element is added to the queue, all indexes in the map
 371     // become invalid. Namely, the new element is assigned with an index of 1,
 372     // and each index i, i > 1 becomes shifted by 1 to the left:
 373     //
 374     //     1, 1, 2, 3, ... , n-1, n
 375     //
 376     // Re-establishing the invariant would seem to require a pass through the
 377     // map incrementing all indexes (map values) by 1, which is O(n).
 378     //
 379     // The good news is we can do much better then this!
 380     //
 381     // Let's create a single field of type long, called 'counter'. Then each
 382     // time a new element 'x' is added to the queue, a value of this field gets
 383     // incremented. Then the resulting value of the 'counter_x' is then put as a
 384     // value under key 'x' to the map:
 385     //
 386     //    map.put(x, counter_x)
 387     //
 388     // It gives us a map that maps an element to a value the counter had at the
 389     // time the element had been added.
 390     //
 391     // In order to retrieve an index of any element 'x' in the queue (at any
 392     // given time) we simply need to subtract the value (the snapshot of the
 393     // counter at the time when the 'x' was added) from the current value of the
 394     // counter. This operation basically answers the question:
 395     //
 396     //     How many elements ago 'x' was the tail of the queue?
 397     //
 398     // Which is the same as its index in the queue now. Given, of course, it's
 399     // still in the queue.
 400     //
 401     // I'm pretty sure in a real life long overflow will never happen, so it's
 402     // not too practical to add recalibrating code, but a pedantic person might
 403     // want to do so:
 404     //
 405     //     if (counter == Long.MAX_VALUE) {
 406     //         recalibrate();
 407     //     }
 408     //
 409     // Where 'recalibrate()' goes through the table doing this:
 410     //
 411     //     value -= counter
 412     //
 413     // That's given, of course, the size of the table itself is less than
 414     // Long.MAX_VALUE :-)
 415     //
 416     private static final class Table {
 417 
 418         private final Map<String, Map<String, Long>> map;
 419         private final CircularBuffer<HeaderField> buffer;
 420         private long counter = 1;
 421 
 422         Table(int capacity) {
 423             buffer = new CircularBuffer<>(capacity);
 424             map = new HashMap<>(capacity);
 425         }
 426 
 427         void add(HeaderField f) {
 428             buffer.add(f);
 429             Map<String, Long> values = map.computeIfAbsent(f.name, k -> new HashMap<>());
 430             values.put(f.value, counter++);
 431         }
 432 
 433         HeaderField get(int index) {
 434             return buffer.get(index - 1);
 435         }
 436 
 437         int indexOf(String name, String value) {
 438             Map<String, Long> values = map.get(name);
 439             if (values == null) {
 440                 return 0;
 441             }
 442             Long index = values.get(value);
 443             if (index != null) {
 444                 return (int) (counter - index);
 445             } else {
 446                 assert !values.isEmpty();
 447                 Long any = values.values().iterator().next(); // Iterator allocation
 448                 return -(int) (counter - any);
 449             }
 450         }
 451 
 452         HeaderField remove() {
 453             HeaderField f = buffer.remove();
 454             Map<String, Long> values = map.get(f.name);
 455             Long index = values.remove(f.value);
 456             assert index != null;
 457             if (values.isEmpty()) {
 458                 map.remove(f.name);
 459             }
 460             return f;
 461         }
 462 
 463         int size() {
 464             return buffer.size;
 465         }
 466 
 467         public void setCapacity(int capacity) {
 468             buffer.resize(capacity);
 469         }
 470     }
 471 
 472     //                    head
 473     //                    v
 474     // [ ][ ][A][B][C][D][ ][ ][ ]
 475     //        ^
 476     //        tail
 477     //
 478     //       |<- size ->| (4)
 479     // |<------ capacity ------->| (9)
 480     //
 481     static final class CircularBuffer<E> {
 482 
 483         int tail, head, size, capacity;
 484         Object[] elements;
 485 
 486         CircularBuffer(int capacity) {
 487             this.capacity = capacity;
 488             elements = new Object[capacity];
 489         }
 490 
 491         void add(E elem) {
 492             if (size == capacity) {
 493                 throw new IllegalStateException(
 494                         format("No room for '%s': capacity=%s", elem, capacity));
 495             }
 496             elements[head] = elem;
 497             head = (head + 1) % capacity;
 498             size++;
 499         }
 500 
 501         @SuppressWarnings("unchecked")
 502         E remove() {
 503             if (size == 0) {
 504                 throw new NoSuchElementException("Empty");
 505             }
 506             E elem = (E) elements[tail];
 507             elements[tail] = null;
 508             tail = (tail + 1) % capacity;
 509             size--;
 510             return elem;
 511         }
 512 
 513         @SuppressWarnings("unchecked")
 514         E get(int index) {
 515             if (index < 0 || index >= size) {
 516                 throw new IndexOutOfBoundsException(
 517                         format("0 <= index <= capacity: index=%s, capacity=%s",
 518                                 index, capacity));
 519             }
 520             int idx = (tail + (size - index - 1)) % capacity;
 521             return (E) elements[idx];
 522         }
 523 
 524         public void resize(int newCapacity) {
 525             if (newCapacity < size) {
 526                 throw new IllegalStateException(
 527                         format("newCapacity >= size: newCapacity=%s, size=%s",
 528                                 newCapacity, size));
 529             }
 530 
 531             Object[] newElements = new Object[newCapacity];
 532 
 533             if (tail < head || size == 0) {
 534                 System.arraycopy(elements, tail, newElements, 0, size);
 535             } else {
 536                 System.arraycopy(elements, tail, newElements, 0, elements.length - tail);
 537                 System.arraycopy(elements, 0, newElements, elements.length - tail, head);
 538             }
 539 
 540             elements = newElements;
 541             tail = 0;
 542             head = size;
 543             this.capacity = newCapacity;
 544         }
 545     }
 546 }