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.internal.vm.annotation.Stable; 28 29 import java.io.IOException; 30 import java.io.UncheckedIOException; 31 import java.net.ProtocolException; 32 import java.nio.ByteBuffer; 33 34 import static java.lang.String.format; 35 import static java.util.Objects.requireNonNull; 36 37 /** 38 * Decodes headers from their binary representation. 39 * 40 * <p>Typical lifecycle looks like this: 41 * 42 * <p> {@link #Decoder(int) new Decoder} 43 * ({@link #setMaxCapacity(int) setMaxCapacity}? 44 * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* 45 * 46 * @apiNote 47 * 48 * <p> The design intentions behind Decoder were to facilitate flexible and 49 * incremental style of processing. 50 * 51 * <p> {@code Decoder} does not require a complete header block in a single 52 * {@code ByteBuffer}. The header block can be spread across many buffers of any 53 * size and decoded one-by-one the way it makes most sense for the user. This 54 * way also allows not to limit the size of the header block. 55 * 56 * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as 57 * soon as they become decoded. Using the callback also gives the user a freedom 58 * to decide how headers are processed. The callback does not limit the number 59 * of headers decoded during single decoding operation. 60 * 61 * @since 9 62 */ 63 public final class Decoder { 64 65 @Stable 66 private static final State[] states = new State[256]; 67 68 static { 69 // To be able to do a quick lookup, each of 256 possibilities are mapped 70 // to corresponding states. 71 // 72 // We can safely do this since patterns 1, 01, 001, 0001, 0000 are 73 // Huffman prefixes and therefore are inherently not ambiguous. 74 // 75 // I do it mainly for better debugging (to not go each time step by step 76 // through if...else tree). As for performance win for the decoding, I 77 // believe is negligible. 78 for (int i = 0; i < states.length; i++) { 79 if ((i & 0b1000_0000) == 0b1000_0000) { 80 states[i] = State.INDEXED; 81 } else if ((i & 0b1100_0000) == 0b0100_0000) { 82 states[i] = State.LITERAL_WITH_INDEXING; 83 } else if ((i & 0b1110_0000) == 0b0010_0000) { 84 states[i] = State.SIZE_UPDATE; 85 } else if ((i & 0b1111_0000) == 0b0001_0000) { 86 states[i] = State.LITERAL_NEVER_INDEXED; 87 } else if ((i & 0b1111_0000) == 0b0000_0000) { 88 states[i] = State.LITERAL; 89 } else { 90 throw new InternalError(String.valueOf(i)); 91 } 92 } 93 } 94 95 private final HeaderTable table; 96 97 private State state = State.READY; 98 private final IntegerReader integerReader; 99 private final StringReader stringReader; 100 private final StringBuilder name; 101 private final StringBuilder value; 102 private int intValue; 103 private boolean firstValueRead; 104 private boolean firstValueIndex; 105 private boolean nameHuffmanEncoded; 106 private boolean valueHuffmanEncoded; 107 private int capacity; 108 109 /** 110 * Constructs a {@code Decoder} with the specified initial capacity of the 111 * header table. 112 * 113 * <p> The value has to be agreed between decoder and encoder out-of-band, 114 * e.g. by a protocol that uses HPACK (see <a 115 * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table 116 * Size</a>). 117 * 118 * @param capacity 119 * a non-negative integer 120 * 121 * @throws IllegalArgumentException 122 * if capacity is negative 123 */ 124 public Decoder(int capacity) { 125 setMaxCapacity(capacity); 126 table = new HeaderTable(capacity); 127 integerReader = new IntegerReader(); 128 stringReader = new StringReader(); 129 name = new StringBuilder(512); 130 value = new StringBuilder(1024); 131 } 132 133 /** 134 * Sets a maximum capacity of the header table. 135 * 136 * <p> The value has to be agreed between decoder and encoder out-of-band, 137 * e.g. by a protocol that uses HPACK (see <a 138 * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table 139 * Size</a>). 140 * 141 * @param capacity 142 * a non-negative integer 143 * 144 * @throws IllegalArgumentException 145 * if capacity is negative 146 */ 147 public void setMaxCapacity(int capacity) { 148 if (capacity < 0) { 149 throw new IllegalArgumentException("capacity >= 0: " + capacity); 150 } 151 // FIXME: await capacity update if less than what was prior to it 152 this.capacity = capacity; 153 } 154 155 /** 156 * Decodes a header block from the given buffer to the given callback. 157 * 158 * <p> Suppose a header block is represented by a sequence of {@code 159 * ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the 160 * consumer of decoded headers is represented by the callback. Then to 161 * decode the header block, the following approach might be used: 162 * 163 * <pre>{@code 164 * while (buffers.hasNext()) { 165 * ByteBuffer input = buffers.next(); 166 * decoder.decode(input, callback, !buffers.hasNext()); 167 * } 168 * }</pre> 169 * 170 * <p> The decoder reads as much as possible of the header block from the 171 * given buffer, starting at the buffer's position, and increments its 172 * position to reflect the bytes read. The buffer's mark and limit will not 173 * be modified. 174 * 175 * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the 176 * current header block is deemed ended, and inconsistencies, if any, are 177 * reported immediately by throwing an {@code UncheckedIOException}. 178 * 179 * <p> Each callback method is called only after the implementation has 180 * processed the corresponding bytes. If the bytes revealed a decoding 181 * error, the callback method is not called. 182 * 183 * <p> In addition to exceptions thrown directly by the method, any 184 * exceptions thrown from the {@code callback} will bubble up. 185 * 186 * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of 187 * returning it for two reasons. The first one is that the user of the 188 * decoder always knows which chunk is the last. The second one is to throw 189 * the most detailed exception possible, which might be useful for 190 * diagnosing issues. 191 * 192 * @implNote This implementation is not atomic in respect to decoding 193 * errors. In other words, if the decoding operation has thrown a decoding 194 * error, the decoder is no longer usable. 195 * 196 * @param headerBlock 197 * the chunk of the header block, may be empty 198 * @param endOfHeaderBlock 199 * true if the chunk is the final (or the only one) in the sequence 200 * 201 * @param consumer 202 * the callback 203 * @throws UncheckedIOException 204 * in case of a decoding error 205 * @throws NullPointerException 206 * if either headerBlock or consumer are null 207 */ 208 public void decode(ByteBuffer headerBlock, boolean endOfHeaderBlock, 209 DecodingCallback consumer) { 210 requireNonNull(headerBlock, "headerBlock"); 211 requireNonNull(consumer, "consumer"); 212 while (headerBlock.hasRemaining()) { 213 proceed(headerBlock, consumer); 214 } 215 if (endOfHeaderBlock && state != State.READY) { 216 throw new UncheckedIOException( 217 new ProtocolException("Unexpected end of header block")); 218 } 219 } 220 221 private void proceed(ByteBuffer input, DecodingCallback action) { 222 switch (state) { 223 case READY: 224 resumeReady(input); 225 break; 226 case INDEXED: 227 resumeIndexed(input, action); 228 break; 229 case LITERAL: 230 resumeLiteral(input, action); 231 break; 232 case LITERAL_WITH_INDEXING: 233 resumeLiteralWithIndexing(input, action); 234 break; 235 case LITERAL_NEVER_INDEXED: 236 resumeLiteralNeverIndexed(input, action); 237 break; 238 case SIZE_UPDATE: 239 resumeSizeUpdate(input, action); 240 break; 241 default: 242 throw new InternalError( 243 "Unexpected decoder state: " + String.valueOf(state)); 244 } 245 } 246 247 private void resumeReady(ByteBuffer input) { 248 int b = input.get(input.position()) & 0xff; // absolute read 249 State s = states[b]; 250 switch (s) { 251 case INDEXED: 252 integerReader.configure(7); 253 state = State.INDEXED; 254 firstValueIndex = true; 255 break; 256 case LITERAL: 257 state = State.LITERAL; 258 firstValueIndex = (b & 0b0000_1111) != 0; 259 if (firstValueIndex) { 260 integerReader.configure(4); 261 } 262 break; 263 case LITERAL_WITH_INDEXING: 264 state = State.LITERAL_WITH_INDEXING; 265 firstValueIndex = (b & 0b0011_1111) != 0; 266 if (firstValueIndex) { 267 integerReader.configure(6); 268 } 269 break; 275 } 276 break; 277 case SIZE_UPDATE: 278 integerReader.configure(5); 279 state = State.SIZE_UPDATE; 280 firstValueIndex = true; 281 break; 282 default: 283 throw new InternalError(String.valueOf(s)); 284 } 285 if (!firstValueIndex) { 286 input.get(); // advance, next stop: "String Literal" 287 } 288 } 289 290 // 0 1 2 3 4 5 6 7 291 // +---+---+---+---+---+---+---+---+ 292 // | 1 | Index (7+) | 293 // +---+---------------------------+ 294 // 295 private void resumeIndexed(ByteBuffer input, DecodingCallback action) { 296 if (!integerReader.read(input)) { 297 return; 298 } 299 intValue = integerReader.get(); 300 integerReader.reset(); 301 try { 302 HeaderTable.HeaderField f = table.get(intValue); 303 action.onIndexed(intValue, f.name, f.value); 304 } finally { 305 state = State.READY; 306 } 307 } 308 309 // 0 1 2 3 4 5 6 7 310 // +---+---+---+---+---+---+---+---+ 311 // | 0 | 0 | 0 | 0 | Index (4+) | 312 // +---+---+-----------------------+ 313 // | H | Value Length (7+) | 314 // +---+---------------------------+ 315 // | Value String (Length octets) | 316 // +-------------------------------+ 317 // 318 // 0 1 2 3 4 5 6 7 319 // +---+---+---+---+---+---+---+---+ 320 // | 0 | 0 | 0 | 0 | 0 | 321 // +---+---+-----------------------+ 322 // | H | Name Length (7+) | 323 // +---+---------------------------+ 324 // | Name String (Length octets) | 325 // +---+---------------------------+ 326 // | H | Value Length (7+) | 327 // +---+---------------------------+ 328 // | Value String (Length octets) | 329 // +-------------------------------+ 330 // 331 private void resumeLiteral(ByteBuffer input, DecodingCallback action) { 332 if (!completeReading(input)) { 333 return; 334 } 335 try { 336 if (firstValueIndex) { 337 HeaderTable.HeaderField f = table.get(intValue); 338 action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); 339 } else { 340 action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 341 } 342 } finally { 343 cleanUpAfterReading(); 344 } 345 } 346 347 // 348 // 0 1 2 3 4 5 6 7 349 // +---+---+---+---+---+---+---+---+ 350 // | 0 | 1 | Index (6+) | 351 // +---+---+-----------------------+ 352 // | H | Value Length (7+) | 353 // +---+---------------------------+ 354 // | Value String (Length octets) | 355 // +-------------------------------+ 356 // 357 // 0 1 2 3 4 5 6 7 358 // +---+---+---+---+---+---+---+---+ 359 // | 0 | 1 | 0 | 360 // +---+---+-----------------------+ 361 // | H | Name Length (7+) | 362 // +---+---------------------------+ 363 // | Name String (Length octets) | 364 // +---+---------------------------+ 365 // | H | Value Length (7+) | 366 // +---+---------------------------+ 367 // | Value String (Length octets) | 368 // +-------------------------------+ 369 // 370 private void resumeLiteralWithIndexing(ByteBuffer input, DecodingCallback action) { 371 if (!completeReading(input)) { 372 return; 373 } 374 try { 375 // 376 // 1. (name, value) will be stored in the table as strings 377 // 2. Most likely the callback will also create strings from them 378 // ------------------------------------------------------------------------ 379 // Let's create those string beforehand (and only once!) to benefit everyone 380 // 381 String n; 382 String v = value.toString(); 383 if (firstValueIndex) { 384 HeaderTable.HeaderField f = table.get(intValue); 385 n = f.name; 386 action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); 387 } else { 388 n = name.toString(); 389 action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); 390 } 391 table.put(n, v); 392 } catch (IllegalArgumentException | IllegalStateException e) { 393 throw new UncheckedIOException( 394 (IOException) new ProtocolException().initCause(e)); 395 } finally { 396 cleanUpAfterReading(); 397 } 398 } 399 400 // 0 1 2 3 4 5 6 7 401 // +---+---+---+---+---+---+---+---+ 402 // | 0 | 0 | 0 | 1 | Index (4+) | 403 // +---+---+-----------------------+ 404 // | H | Value Length (7+) | 405 // +---+---------------------------+ 406 // | Value String (Length octets) | 407 // +-------------------------------+ 408 // 409 // 0 1 2 3 4 5 6 7 410 // +---+---+---+---+---+---+---+---+ 411 // | 0 | 0 | 0 | 1 | 0 | 412 // +---+---+-----------------------+ 413 // | H | Name Length (7+) | 414 // +---+---------------------------+ 415 // | Name String (Length octets) | 416 // +---+---------------------------+ 417 // | H | Value Length (7+) | 418 // +---+---------------------------+ 419 // | Value String (Length octets) | 420 // +-------------------------------+ 421 // 422 private void resumeLiteralNeverIndexed(ByteBuffer input, DecodingCallback action) { 423 if (!completeReading(input)) { 424 return; 425 } 426 try { 427 if (firstValueIndex) { 428 HeaderTable.HeaderField f = table.get(intValue); 429 action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); 430 } else { 431 action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 432 } 433 } finally { 434 cleanUpAfterReading(); 435 } 436 } 437 438 // 0 1 2 3 4 5 6 7 439 // +---+---+---+---+---+---+---+---+ 440 // | 0 | 0 | 1 | Max size (5+) | 441 // +---+---------------------------+ 442 // 443 private void resumeSizeUpdate(ByteBuffer input, DecodingCallback action) { 444 if (!integerReader.read(input)) { 445 return; 446 } 447 intValue = integerReader.get(); 448 assert intValue >= 0; 449 if (intValue > capacity) { 450 throw new UncheckedIOException(new ProtocolException( 451 format("Received capacity exceeds expected: " + 452 "capacity=%s, expected=%s", intValue, capacity))); 453 } 454 integerReader.reset(); 455 try { 456 action.onSizeUpdate(intValue); 457 table.setMaxSize(intValue); 458 } finally { 459 state = State.READY; 460 } 461 } 462 463 private boolean completeReading(ByteBuffer input) { 464 if (!firstValueRead) { 465 if (firstValueIndex) { 466 if (!integerReader.read(input)) { 467 return false; 468 } 469 intValue = integerReader.get(); 470 integerReader.reset(); 471 } else { 472 if (!stringReader.read(input, name)) { 473 return false; 474 } 475 nameHuffmanEncoded = stringReader.isHuffmanEncoded(); 476 stringReader.reset(); 477 } 478 firstValueRead = true; 479 return false; 480 } else { 481 if (!stringReader.read(input, value)) { 482 return false; 483 } | 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.io.IOException; 31 import java.nio.ByteBuffer; 32 import java.util.concurrent.atomic.AtomicLong; 33 34 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA; 35 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL; 36 import static java.lang.String.format; 37 import static java.util.Objects.requireNonNull; 38 39 /** 40 * Decodes headers from their binary representation. 41 * 42 * <p> Typical lifecycle looks like this: 43 * 44 * <p> {@link #Decoder(int) new Decoder} 45 * ({@link #setMaxCapacity(int) setMaxCapacity}? 46 * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* 47 * 48 * @apiNote 49 * 50 * <p> The design intentions behind Decoder were to facilitate flexible and 51 * incremental style of processing. 52 * 53 * <p> {@code Decoder} does not require a complete header block in a single 54 * {@code ByteBuffer}. The header block can be spread across many buffers of any 55 * size and decoded one-by-one the way it makes most sense for the user. This 56 * way also allows not to limit the size of the header block. 57 * 58 * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as 59 * soon as they become decoded. Using the callback also gives the user a freedom 60 * to decide how headers are processed. The callback does not limit the number 61 * of headers decoded during single decoding operation. 62 * 63 * @since 9 64 */ 65 public final class Decoder { 66 67 private final Logger logger; 68 private static final AtomicLong DECODERS_IDS = new AtomicLong(); 69 70 @Stable 71 private static final State[] states = new State[256]; 72 73 static { 74 // To be able to do a quick lookup, each of 256 possibilities are mapped 75 // to corresponding states. 76 // 77 // We can safely do this since patterns 1, 01, 001, 0001, 0000 are 78 // Huffman prefixes and therefore are inherently not ambiguous. 79 // 80 // I do it mainly for better debugging (to not go each time step by step 81 // through if...else tree). As for performance win for the decoding, I 82 // believe is negligible. 83 for (int i = 0; i < states.length; i++) { 84 if ((i & 0b1000_0000) == 0b1000_0000) { 85 states[i] = State.INDEXED; 86 } else if ((i & 0b1100_0000) == 0b0100_0000) { 87 states[i] = State.LITERAL_WITH_INDEXING; 88 } else if ((i & 0b1110_0000) == 0b0010_0000) { 89 states[i] = State.SIZE_UPDATE; 90 } else if ((i & 0b1111_0000) == 0b0001_0000) { 91 states[i] = State.LITERAL_NEVER_INDEXED; 92 } else if ((i & 0b1111_0000) == 0b0000_0000) { 93 states[i] = State.LITERAL; 94 } else { 95 throw new InternalError(String.valueOf(i)); 96 } 97 } 98 } 99 100 private final long id; 101 private final HeaderTable table; 102 103 private State state = State.READY; 104 private final IntegerReader integerReader; 105 private final StringReader stringReader; 106 private final StringBuilder name; 107 private final StringBuilder value; 108 private int intValue; 109 private boolean firstValueRead; 110 private boolean firstValueIndex; 111 private boolean nameHuffmanEncoded; 112 private boolean valueHuffmanEncoded; 113 private int capacity; 114 115 /** 116 * Constructs a {@code Decoder} with the specified initial capacity of the 117 * header table. 118 * 119 * <p> The value has to be agreed between decoder and encoder out-of-band, 120 * e.g. by a protocol that uses HPACK 121 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). 122 * 123 * @param capacity 124 * a non-negative integer 125 * 126 * @throws IllegalArgumentException 127 * if capacity is negative 128 */ 129 public Decoder(int capacity) { 130 id = DECODERS_IDS.incrementAndGet(); 131 logger = HPACK.getLogger().subLogger("Decoder#" + id); 132 if (logger.isLoggable(NORMAL)) { 133 logger.log(NORMAL, () -> format("new decoder with maximum table size %s", 134 capacity)); 135 } 136 if (logger.isLoggable(NORMAL)) { 137 /* To correlate with logging outside HPACK, knowing 138 hashCode/toString is important */ 139 logger.log(NORMAL, () -> { 140 String hashCode = Integer.toHexString( 141 System.identityHashCode(this)); 142 return format("toString='%s', identityHashCode=%s", 143 toString(), hashCode); 144 }); 145 } 146 setMaxCapacity0(capacity); 147 table = new HeaderTable(capacity, logger.subLogger("HeaderTable")); 148 integerReader = new IntegerReader(); 149 stringReader = new StringReader(); 150 name = new StringBuilder(512); 151 value = new StringBuilder(1024); 152 } 153 154 /** 155 * Sets a maximum capacity of the header table. 156 * 157 * <p> The value has to be agreed between decoder and encoder out-of-band, 158 * e.g. by a protocol that uses HPACK 159 * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>). 160 * 161 * @param capacity 162 * a non-negative integer 163 * 164 * @throws IllegalArgumentException 165 * if capacity is negative 166 */ 167 public void setMaxCapacity(int capacity) { 168 if (logger.isLoggable(NORMAL)) { 169 logger.log(NORMAL, () -> format("setting maximum table size to %s", 170 capacity)); 171 } 172 setMaxCapacity0(capacity); 173 } 174 175 private void setMaxCapacity0(int capacity) { 176 if (capacity < 0) { 177 throw new IllegalArgumentException("capacity >= 0: " + capacity); 178 } 179 // FIXME: await capacity update if less than what was prior to it 180 this.capacity = capacity; 181 } 182 183 /** 184 * Decodes a header block from the given buffer to the given callback. 185 * 186 * <p> Suppose a header block is represented by a sequence of 187 * {@code ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the 188 * consumer of decoded headers is represented by the callback. Then to 189 * decode the header block, the following approach might be used: 190 * 191 * <pre>{@code 192 * while (buffers.hasNext()) { 193 * ByteBuffer input = buffers.next(); 194 * decoder.decode(input, callback, !buffers.hasNext()); 195 * } 196 * }</pre> 197 * 198 * <p> The decoder reads as much as possible of the header block from the 199 * given buffer, starting at the buffer's position, and increments its 200 * position to reflect the bytes read. The buffer's mark and limit will not 201 * be modified. 202 * 203 * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the 204 * current header block is deemed ended, and inconsistencies, if any, are 205 * reported immediately by throwing an {@code IOException}. 206 * 207 * <p> Each callback method is called only after the implementation has 208 * processed the corresponding bytes. If the bytes revealed a decoding 209 * error, the callback method is not called. 210 * 211 * <p> In addition to exceptions thrown directly by the method, any 212 * exceptions thrown from the {@code callback} will bubble up. 213 * 214 * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of 215 * returning it for two reasons. The first one is that the user of the 216 * decoder always knows which chunk is the last. The second one is to throw 217 * the most detailed exception possible, which might be useful for 218 * diagnosing issues. 219 * 220 * @implNote This implementation is not atomic in respect to decoding 221 * errors. In other words, if the decoding operation has thrown a decoding 222 * error, the decoder is no longer usable. 223 * 224 * @param headerBlock 225 * the chunk of the header block, may be empty 226 * @param endOfHeaderBlock 227 * true if the chunk is the final (or the only one) in the sequence 228 * 229 * @param consumer 230 * the callback 231 * @throws IOException 232 * in case of a decoding error 233 * @throws NullPointerException 234 * if either headerBlock or consumer are null 235 */ 236 public void decode(ByteBuffer headerBlock, 237 boolean endOfHeaderBlock, 238 DecodingCallback consumer) throws IOException { 239 requireNonNull(headerBlock, "headerBlock"); 240 requireNonNull(consumer, "consumer"); 241 if (logger.isLoggable(NORMAL)) { 242 logger.log(NORMAL, () -> format("reading %s, end of header block? %s", 243 headerBlock, endOfHeaderBlock)); 244 } 245 while (headerBlock.hasRemaining()) { 246 proceed(headerBlock, consumer); 247 } 248 if (endOfHeaderBlock && state != State.READY) { 249 logger.log(NORMAL, () -> format("unexpected end of %s representation", 250 state)); 251 throw new IOException("Unexpected end of header block"); 252 } 253 } 254 255 private void proceed(ByteBuffer input, DecodingCallback action) 256 throws IOException { 257 switch (state) { 258 case READY: 259 resumeReady(input); 260 break; 261 case INDEXED: 262 resumeIndexed(input, action); 263 break; 264 case LITERAL: 265 resumeLiteral(input, action); 266 break; 267 case LITERAL_WITH_INDEXING: 268 resumeLiteralWithIndexing(input, action); 269 break; 270 case LITERAL_NEVER_INDEXED: 271 resumeLiteralNeverIndexed(input, action); 272 break; 273 case SIZE_UPDATE: 274 resumeSizeUpdate(input, action); 275 break; 276 default: 277 throw new InternalError("Unexpected decoder state: " + state); 278 } 279 } 280 281 private void resumeReady(ByteBuffer input) { 282 int b = input.get(input.position()) & 0xff; // absolute read 283 State s = states[b]; 284 if (logger.isLoggable(EXTRA)) { 285 logger.log(EXTRA, () -> format("next binary representation %s (first byte 0x%02x)", 286 s, b)); 287 } 288 switch (s) { 289 case INDEXED: 290 integerReader.configure(7); 291 state = State.INDEXED; 292 firstValueIndex = true; 293 break; 294 case LITERAL: 295 state = State.LITERAL; 296 firstValueIndex = (b & 0b0000_1111) != 0; 297 if (firstValueIndex) { 298 integerReader.configure(4); 299 } 300 break; 301 case LITERAL_WITH_INDEXING: 302 state = State.LITERAL_WITH_INDEXING; 303 firstValueIndex = (b & 0b0011_1111) != 0; 304 if (firstValueIndex) { 305 integerReader.configure(6); 306 } 307 break; 313 } 314 break; 315 case SIZE_UPDATE: 316 integerReader.configure(5); 317 state = State.SIZE_UPDATE; 318 firstValueIndex = true; 319 break; 320 default: 321 throw new InternalError(String.valueOf(s)); 322 } 323 if (!firstValueIndex) { 324 input.get(); // advance, next stop: "String Literal" 325 } 326 } 327 328 // 0 1 2 3 4 5 6 7 329 // +---+---+---+---+---+---+---+---+ 330 // | 1 | Index (7+) | 331 // +---+---------------------------+ 332 // 333 private void resumeIndexed(ByteBuffer input, DecodingCallback action) 334 throws IOException { 335 if (!integerReader.read(input)) { 336 return; 337 } 338 intValue = integerReader.get(); 339 integerReader.reset(); 340 if (logger.isLoggable(NORMAL)) { 341 logger.log(NORMAL, () -> format("indexed %s", intValue)); 342 } 343 try { 344 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 345 action.onIndexed(intValue, f.name, f.value); 346 } finally { 347 state = State.READY; 348 } 349 } 350 351 private HeaderTable.HeaderField getHeaderFieldAt(int index) 352 throws IOException 353 { 354 HeaderTable.HeaderField f; 355 try { 356 f = table.get(index); 357 } catch (IndexOutOfBoundsException e) { 358 throw new IOException("header fields table index", e); 359 } 360 return f; 361 } 362 363 // 0 1 2 3 4 5 6 7 364 // +---+---+---+---+---+---+---+---+ 365 // | 0 | 0 | 0 | 0 | Index (4+) | 366 // +---+---+-----------------------+ 367 // | H | Value Length (7+) | 368 // +---+---------------------------+ 369 // | Value String (Length octets) | 370 // +-------------------------------+ 371 // 372 // 0 1 2 3 4 5 6 7 373 // +---+---+---+---+---+---+---+---+ 374 // | 0 | 0 | 0 | 0 | 0 | 375 // +---+---+-----------------------+ 376 // | H | Name Length (7+) | 377 // +---+---------------------------+ 378 // | Name String (Length octets) | 379 // +---+---------------------------+ 380 // | H | Value Length (7+) | 381 // +---+---------------------------+ 382 // | Value String (Length octets) | 383 // +-------------------------------+ 384 // 385 private void resumeLiteral(ByteBuffer input, DecodingCallback action) 386 throws IOException { 387 if (!completeReading(input)) { 388 return; 389 } 390 try { 391 if (firstValueIndex) { 392 if (logger.isLoggable(NORMAL)) { 393 logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", 394 intValue, value)); 395 } 396 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 397 action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); 398 } else { 399 if (logger.isLoggable(NORMAL)) { 400 logger.log(NORMAL, () -> format("literal without indexing ('%s', '%s')", 401 name, value)); 402 } 403 action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 404 } 405 } finally { 406 cleanUpAfterReading(); 407 } 408 } 409 410 // 411 // 0 1 2 3 4 5 6 7 412 // +---+---+---+---+---+---+---+---+ 413 // | 0 | 1 | Index (6+) | 414 // +---+---+-----------------------+ 415 // | H | Value Length (7+) | 416 // +---+---------------------------+ 417 // | Value String (Length octets) | 418 // +-------------------------------+ 419 // 420 // 0 1 2 3 4 5 6 7 421 // +---+---+---+---+---+---+---+---+ 422 // | 0 | 1 | 0 | 423 // +---+---+-----------------------+ 424 // | H | Name Length (7+) | 425 // +---+---------------------------+ 426 // | Name String (Length octets) | 427 // +---+---------------------------+ 428 // | H | Value Length (7+) | 429 // +---+---------------------------+ 430 // | Value String (Length octets) | 431 // +-------------------------------+ 432 // 433 private void resumeLiteralWithIndexing(ByteBuffer input, 434 DecodingCallback action) 435 throws IOException { 436 if (!completeReading(input)) { 437 return; 438 } 439 try { 440 // 441 // 1. (name, value) will be stored in the table as strings 442 // 2. Most likely the callback will also create strings from them 443 // ------------------------------------------------------------------------ 444 // Let's create those string beforehand (and only once!) to benefit everyone 445 // 446 String n; 447 String v = value.toString(); 448 if (firstValueIndex) { 449 if (logger.isLoggable(NORMAL)) { 450 logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", 451 intValue, value)); 452 } 453 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 454 n = f.name; 455 action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); 456 } else { 457 n = name.toString(); 458 if (logger.isLoggable(NORMAL)) { 459 logger.log(NORMAL, () -> format("literal with incremental indexing ('%s', '%s')", 460 n, value)); 461 } 462 action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); 463 } 464 table.put(n, v); 465 } finally { 466 cleanUpAfterReading(); 467 } 468 } 469 470 // 0 1 2 3 4 5 6 7 471 // +---+---+---+---+---+---+---+---+ 472 // | 0 | 0 | 0 | 1 | Index (4+) | 473 // +---+---+-----------------------+ 474 // | H | Value Length (7+) | 475 // +---+---------------------------+ 476 // | Value String (Length octets) | 477 // +-------------------------------+ 478 // 479 // 0 1 2 3 4 5 6 7 480 // +---+---+---+---+---+---+---+---+ 481 // | 0 | 0 | 0 | 1 | 0 | 482 // +---+---+-----------------------+ 483 // | H | Name Length (7+) | 484 // +---+---------------------------+ 485 // | Name String (Length octets) | 486 // +---+---------------------------+ 487 // | H | Value Length (7+) | 488 // +---+---------------------------+ 489 // | Value String (Length octets) | 490 // +-------------------------------+ 491 // 492 private void resumeLiteralNeverIndexed(ByteBuffer input, 493 DecodingCallback action) 494 throws IOException { 495 if (!completeReading(input)) { 496 return; 497 } 498 try { 499 if (firstValueIndex) { 500 if (logger.isLoggable(NORMAL)) { 501 logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", 502 intValue, value)); 503 } 504 HeaderTable.HeaderField f = getHeaderFieldAt(intValue); 505 action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); 506 } else { 507 if (logger.isLoggable(NORMAL)) { 508 logger.log(NORMAL, () -> format("literal never indexed ('%s', '%s')", 509 name, value)); 510 } 511 action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); 512 } 513 } finally { 514 cleanUpAfterReading(); 515 } 516 } 517 518 // 0 1 2 3 4 5 6 7 519 // +---+---+---+---+---+---+---+---+ 520 // | 0 | 0 | 1 | Max size (5+) | 521 // +---+---------------------------+ 522 // 523 private void resumeSizeUpdate(ByteBuffer input, 524 DecodingCallback action) throws IOException { 525 if (!integerReader.read(input)) { 526 return; 527 } 528 intValue = integerReader.get(); 529 if (logger.isLoggable(NORMAL)) { 530 logger.log(NORMAL, () -> format("dynamic table size update %s", 531 intValue)); 532 } 533 assert intValue >= 0; 534 if (intValue > capacity) { 535 throw new IOException( 536 format("Received capacity exceeds expected: capacity=%s, expected=%s", 537 intValue, capacity)); 538 } 539 integerReader.reset(); 540 try { 541 action.onSizeUpdate(intValue); 542 table.setMaxSize(intValue); 543 } finally { 544 state = State.READY; 545 } 546 } 547 548 private boolean completeReading(ByteBuffer input) throws IOException { 549 if (!firstValueRead) { 550 if (firstValueIndex) { 551 if (!integerReader.read(input)) { 552 return false; 553 } 554 intValue = integerReader.get(); 555 integerReader.reset(); 556 } else { 557 if (!stringReader.read(input, name)) { 558 return false; 559 } 560 nameHuffmanEncoded = stringReader.isHuffmanEncoded(); 561 stringReader.reset(); 562 } 563 firstValueRead = true; 564 return false; 565 } else { 566 if (!stringReader.read(input, value)) { 567 return false; 568 } |