diff --git a/jetty-http/src/main/java/module-info.java b/jetty-http/src/main/java/module-info.java index 396d16bab3c..59164fc20be 100644 --- a/jetty-http/src/main/java/module-info.java +++ b/jetty-http/src/main/java/module-info.java @@ -18,6 +18,7 @@ module org.eclipse.jetty.http requires transitive org.eclipse.jetty.io; exports org.eclipse.jetty.http; + exports org.eclipse.jetty.http.compression; exports org.eclipse.jetty.http.pathmap; uses org.eclipse.jetty.http.HttpFieldPreEncoder; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java index 305c8b59fce..725722fd259 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java @@ -231,5 +231,49 @@ public class HttpTokens } } } + + /** + * This is used when decoding to not decode illegal characters based on RFC9110. + * CR, LF, or NUL are replaced with ' ', all other control and multibyte characters + * are replaced with '?'. If this is given a legal character the same value will be returned. + *
+     * field-vchar = VCHAR / obs-text
+     * obs-text    = %x80-FF
+     * VCHAR       = %x21-7E
+     * 
+ * @param c the character to test. + * @return the original character or the replacement character ' ' or '?', + * the return value is guaranteed to be a valid ISO-8859-1 character. + */ + public static char sanitizeFieldVchar(char c) + { + switch (c) + { + // A recipient of CR, LF, or NUL within a field value MUST either reject the message + // or replace each of those characters with SP before further processing + case '\r': + case '\n': + case 0x00: + return ' '; + + default: + if (isIllegalFieldVchar(c)) + return '?'; + } + return c; + } + + /** + * Checks whether this is an invalid VCHAR based on RFC9110. + * If this not a valid ISO-8859-1 character or a control character + * we say that it is illegal. + * + * @param c the character to test. + * @return true if this is invalid VCHAR. + */ + public static boolean isIllegalFieldVchar(char c) + { + return (c >= 256 || c < ' '); + } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/EncodingException.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java similarity index 92% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/EncodingException.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java index d8cc91a7de7..22dde443355 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/EncodingException.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java @@ -11,7 +11,7 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack.internal.util; +package org.eclipse.jetty.http.compression; public class EncodingException extends Exception { diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java similarity index 82% rename from jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java index 8b880b10ebe..bf530959175 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java @@ -11,14 +11,16 @@ // ======================================================================== // -package org.eclipse.jetty.http2.hpack; - -import java.nio.ByteBuffer; - -import org.eclipse.jetty.util.Utf8StringBuilder; +package org.eclipse.jetty.http.compression; +/** + * This class contains the Huffman Codes defined in RFC7541. + */ public class Huffman { + private Huffman() + { + } // Appendix C: Huffman Codes // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C @@ -286,7 +288,7 @@ public class Huffman static final int[][] LCCODES = new int[CODES.length][]; static final char EOS = 256; - // Huffman decode tree stored in a flattened char array for good + // Huffman decode tree stored in a flattened char array for good // locality of reference. static final char[] tree; static final char[] rowsym; @@ -302,9 +304,9 @@ public class Huffman } int r = 0; - for (int i = 0; i < CODES.length; i++) + for (int[] ints : CODES) { - r += (CODES[i][1] + 7) / 8; + r += (ints[1] + 7) / 8; } tree = new char[r * 256]; rowsym = new char[r]; @@ -347,200 +349,4 @@ public class Huffman } } } - - public static String decode(ByteBuffer buffer) throws HpackException.CompressionException - { - return decode(buffer, buffer.remaining()); - } - - public static String decode(ByteBuffer buffer, int length) throws HpackException.CompressionException - { - Utf8StringBuilder utf8 = new Utf8StringBuilder(length * 2); - int node = 0; - int current = 0; - int bits = 0; - - for (int i = 0; i < length; i++) - { - int b = buffer.get() & 0xFF; - current = (current << 8) | b; - bits += 8; - while (bits >= 8) - { - int c = (current >>> (bits - 8)) & 0xFF; - node = tree[node * 256 + c]; - if (rowbits[node] != 0) - { - if (rowsym[node] == EOS) - throw new HpackException.CompressionException("EOS in content"); - - // terminal node - utf8.append((byte)(0xFF & rowsym[node])); - bits -= rowbits[node]; - node = 0; - } - else - { - // non-terminal node - bits -= 8; - } - } - } - - while (bits > 0) - { - int c = (current << (8 - bits)) & 0xFF; - int lastNode = node; - node = tree[node * 256 + c]; - - if (rowbits[node] == 0 || rowbits[node] > bits) - { - int requiredPadding = 0; - for (int i = 0; i < bits; i++) - { - requiredPadding = (requiredPadding << 1) | 1; - } - - if ((c >> (8 - bits)) != requiredPadding) - throw new HpackException.CompressionException("Incorrect padding"); - - node = lastNode; - break; - } - - utf8.append((byte)(0xFF & rowsym[node])); - bits -= rowbits[node]; - node = 0; - } - - if (node != 0) - throw new HpackException.CompressionException("Bad termination"); - - return utf8.toString(); - } - - public static int octetsNeeded(String s) - { - return octetsNeeded(CODES, s); - } - - public static int octetsNeeded(byte[] b) - { - return octetsNeeded(CODES, b); - } - - public static void encode(ByteBuffer buffer, String s) - { - encode(CODES, buffer, s); - } - - public static void encode(ByteBuffer buffer, byte[] b) - { - encode(CODES, buffer, b); - } - - public static int octetsNeededLC(String s) - { - return octetsNeeded(LCCODES, s); - } - - public static void encodeLC(ByteBuffer buffer, String s) - { - encode(LCCODES, buffer, s); - } - - private static int octetsNeeded(final int[][] table, String s) - { - int needed = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - return -1; - needed += table[c][1]; - } - - return (needed + 7) / 8; - } - - private static int octetsNeeded(final int[][] table, byte[] b) - { - int needed = 0; - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - needed += table[c][1]; - } - return (needed + 7) / 8; - } - - /** - * @param table The table to encode by - * @param buffer The buffer to encode to - * @param s The string to encode - */ - private static void encode(final int[][] table, ByteBuffer buffer, String s) - { - long current = 0; - int n = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - throw new IllegalArgumentException(); - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } - - private static void encode(final int[][] table, ByteBuffer buffer, byte[] b) - { - long current = 0; - int n = 0; - - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java similarity index 62% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanDecoder.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java index 31e832ccfa2..983e10a9847 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanDecoder.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java @@ -11,26 +11,34 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack.internal.util; +package org.eclipse.jetty.http.compression; import java.nio.ByteBuffer; -import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.http.HttpTokens; +import org.eclipse.jetty.util.CharsetStringBuilder; +import static org.eclipse.jetty.http.compression.Huffman.rowbits; +import static org.eclipse.jetty.http.compression.Huffman.rowsym; + +/** + *

Used to decoded Huffman encoded strings.

+ * + *

Characters which are illegal field-vchar values are replaced with + * either ' ' or '?' as described in RFC9110

+ */ public class HuffmanDecoder { - static final char EOS = HuffmanEncoder.EOS; - static final char[] tree = HuffmanEncoder.tree; - static final char[] rowsym = HuffmanEncoder.rowsym; - static final byte[] rowbits = HuffmanEncoder.rowbits; - - private final Utf8StringBuilder _utf8 = new Utf8StringBuilder(); + private final CharsetStringBuilder.Iso88591StringBuilder _builder = new CharsetStringBuilder.Iso88591StringBuilder(); private int _length = 0; private int _count = 0; private int _node = 0; private int _current = 0; private int _bits = 0; + /** + * @param length in bytes of the huffman data. + */ public void setLength(int length) { if (_count != 0) @@ -38,6 +46,11 @@ public class HuffmanDecoder _length = length; } + /** + * @param buffer the buffer containing the Huffman encoded bytes. + * @return the decoded String. + * @throws EncodingException if the huffman encoding is invalid. + */ public String decode(ByteBuffer buffer) throws EncodingException { for (; _count < _length; _count++) @@ -50,18 +63,20 @@ public class HuffmanDecoder _bits += 8; while (_bits >= 8) { - int c = (_current >>> (_bits - 8)) & 0xFF; - _node = tree[_node * 256 + c]; + int i = (_current >>> (_bits - 8)) & 0xFF; + _node = Huffman.tree[_node * 256 + i]; if (rowbits[_node] != 0) { - if (rowsym[_node] == EOS) + if (rowsym[_node] == Huffman.EOS) { reset(); throw new EncodingException("eos_in_content"); } // terminal node - _utf8.append((byte)(0xFF & rowsym[_node])); + char c = rowsym[_node]; + c = HttpTokens.sanitizeFieldVchar(c); + _builder.append((byte)c); _bits -= rowbits[_node]; _node = 0; } @@ -75,26 +90,28 @@ public class HuffmanDecoder while (_bits > 0) { - int c = (_current << (8 - _bits)) & 0xFF; + int i = (_current << (8 - _bits)) & 0xFF; int lastNode = _node; - _node = tree[_node * 256 + c]; + _node = Huffman.tree[_node * 256 + i]; if (rowbits[_node] == 0 || rowbits[_node] > _bits) { int requiredPadding = 0; - for (int i = 0; i < _bits; i++) + for (int j = 0; j < _bits; j++) { requiredPadding = (requiredPadding << 1) | 1; } - if ((c >> (8 - _bits)) != requiredPadding) + if ((i >> (8 - _bits)) != requiredPadding) throw new EncodingException("incorrect_padding"); _node = lastNode; break; } - _utf8.append((byte)(0xFF & rowsym[_node])); + char c = rowsym[_node]; + c = HttpTokens.sanitizeFieldVchar(c); + _builder.append((byte)c); _bits -= rowbits[_node]; _node = 0; } @@ -105,14 +122,14 @@ public class HuffmanDecoder throw new EncodingException("bad_termination"); } - String value = _utf8.toString(); + String value = _builder.build(); reset(); return value; } public void reset() { - _utf8.reset(); + _builder.reset(); _count = 0; _current = 0; _node = 0; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java new file mode 100644 index 00000000000..965f90ad017 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java @@ -0,0 +1,137 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http.compression; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http.HttpTokens; + +import static org.eclipse.jetty.http.compression.Huffman.CODES; +import static org.eclipse.jetty.http.compression.Huffman.LCCODES; + +/** + *

Used to encode strings Huffman encoding.

+ * + *

Characters are encoded with ISO-8859-1, if any multi-byte characters or + * control characters are present the encoder will throw {@link EncodingException}.

+ */ +public class HuffmanEncoder +{ + private HuffmanEncoder() + { + } + + /** + * @param s the string to encode. + * @return the number of octets needed to encode the string, or -1 if it cannot be encoded. + */ + public static int octetsNeeded(String s) + { + return octetsNeeded(CODES, s); + } + + /** + * @param b the byte array to encode. + * @return the number of octets needed to encode the bytes, or -1 if it cannot be encoded. + */ + public static int octetsNeeded(byte[] b) + { + int needed = 0; + for (byte value : b) + { + int c = 0xFF & value; + needed += CODES[c][1]; + } + return (needed + 7) / 8; + } + + /** + * @param buffer the buffer to encode into. + * @param s the string to encode. + */ + public static void encode(ByteBuffer buffer, String s) + { + encode(CODES, buffer, s); + } + + /** + * @param s the string to encode in lowercase. + * @return the number of octets needed to encode the string, or -1 if it cannot be encoded. + */ + public static int octetsNeededLowerCase(String s) + { + return octetsNeeded(LCCODES, s); + } + + /** + * @param buffer the buffer to encode into in lowercase. + * @param s the string to encode. + */ + public static void encodeLowerCase(ByteBuffer buffer, String s) + { + encode(LCCODES, buffer, s); + } + + private static int octetsNeeded(final int[][] table, String s) + { + int needed = 0; + int len = s.length(); + for (int i = 0; i < len; i++) + { + char c = s.charAt(i); + if (HttpTokens.isIllegalFieldVchar(c)) + return -1; + needed += table[c][1]; + } + + return (needed + 7) / 8; + } + + /** + * @param table The table to encode by + * @param buffer The buffer to encode to + * @param s The string to encode + */ + private static void encode(final int[][] table, ByteBuffer buffer, String s) + { + long current = 0; + int n = 0; + int len = s.length(); + for (int i = 0; i < len; i++) + { + char c = s.charAt(i); + if (HttpTokens.isIllegalFieldVchar(c)) + throw new IllegalArgumentException(); + int code = table[c][0]; + int bits = table[c][1]; + + current <<= bits; + current |= code; + n += bits; + + while (n >= 8) + { + n -= 8; + buffer.put((byte)(current >> n)); + } + } + + if (n > 0) + { + current <<= (8 - n); + current |= (0xFF >>> n); + buffer.put((byte)(current)); + } + } +} diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java similarity index 59% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerParser.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java index 1088bbc9255..e1c0dbb17bb 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java @@ -11,17 +11,25 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack.internal.util; +package org.eclipse.jetty.http.compression; import java.nio.ByteBuffer; -public class NBitIntegerParser +/** + * Used to decode integers as described in RFC7541. + */ +public class NBitIntegerDecoder { private int _prefix; private long _total; private long _multiplier; private boolean _started; + /** + * Set the prefix length in of the integer representation in bits. + * A prefix of 6 means the integer representation starts after the first 2 bits. + * @param prefix the number of bits in the integer prefix. + */ public void setPrefix(int prefix) { if (_started) @@ -29,11 +37,27 @@ public class NBitIntegerParser _prefix = prefix; } + /** + * Decode an integer from the buffer. If the buffer does not contain the complete integer representation + * a value of -1 is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded integer. + * @return the decoded integer or -1 to indicate that more data is needed. + * @throws ArithmeticException if the value overflows a int. + */ public int decodeInt(ByteBuffer buffer) { return Math.toIntExact(decodeLong(buffer)); } + /** + * Decode a long from the buffer. If the buffer does not contain the complete integer representation + * a value of -1 is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded integer. + * @return the decoded long or -1 to indicate that more data is needed. + * @throws ArithmeticException if the value overflows a long. + */ public long decodeLong(ByteBuffer buffer) { if (!_started) @@ -71,6 +95,9 @@ public class NBitIntegerParser } } + /** + * Reset the internal state of the parser. + */ public void reset() { _prefix = 0; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java similarity index 83% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerEncoder.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java index a23d5426647..0750d6584f8 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitIntegerEncoder.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java @@ -11,13 +11,25 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack.internal.util; +package org.eclipse.jetty.http.compression; import java.nio.ByteBuffer; +/** + * Used to encode integers as described in RFC7541. + */ public class NBitIntegerEncoder { - public static int octectsNeeded(int n, long i) + private NBitIntegerEncoder() + { + } + + /** + * @param n the prefix used to encode this long. + * @param i the integer to encode. + * @return the number of octets it would take to encode the long. + */ + public static int octetsNeeded(int n, long i) { if (n == 8) { @@ -43,6 +55,12 @@ public class NBitIntegerEncoder return (log + 6) / 7; } + /** + * + * @param buf the buffer to encode into. + * @param n the prefix used to encode this long. + * @param i the long to encode into the buffer. + */ public static void encode(ByteBuffer buf, int n, long i) { if (n == 8) diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitStringParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java similarity index 53% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitStringParser.java rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java index 1b5e2a89835..25367abe377 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/NBitStringParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java @@ -11,15 +11,27 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack.internal.util; +package org.eclipse.jetty.http.compression; import java.nio.ByteBuffer; -public class NBitStringParser +import org.eclipse.jetty.util.CharsetStringBuilder; + +/** + *

Used to decode string literals as described in RFC7541.

+ * + *

The string literal representation consists of a single bit to indicate whether huffman encoding is used, + * followed by the string byte length encoded with the n-bit integer representation also from RFC7541, and + * the bytes of the string are directly after this.

+ * + *

Characters which are illegal field-vchar values are replaced with + * either ' ' or '?' as described in RFC9110

+ */ +public class NBitStringDecoder { - private final NBitIntegerParser _integerParser; + private final NBitIntegerDecoder _integerDecoder; private final HuffmanDecoder _huffmanBuilder; - private final StringBuilder _stringBuilder; + private final CharsetStringBuilder.Iso88591StringBuilder _builder; private boolean _huffman; private int _count; private int _length; @@ -34,13 +46,18 @@ public class NBitStringParser VALUE } - public NBitStringParser() + public NBitStringDecoder() { - _integerParser = new NBitIntegerParser(); + _integerDecoder = new NBitIntegerDecoder(); _huffmanBuilder = new HuffmanDecoder(); - _stringBuilder = new StringBuilder(); + _builder = new CharsetStringBuilder.Iso88591StringBuilder(); } + /** + * Set the prefix length in of the string representation in bits. + * A prefix of 6 means the string representation starts after the first 2 bits. + * @param prefix the number of bits in the string prefix. + */ public void setPrefix(int prefix) { if (_state != State.PARSING) @@ -48,6 +65,15 @@ public class NBitStringParser _prefix = prefix; } + /** + * Decode a string from the buffer. If the buffer does not contain the complete string representation + * then a value of null is returned to indicate that more data is needed to complete parsing. + * This should be only after the prefix has been set with {@link #setPrefix(int)}. + * @param buffer the buffer containing the encoded string. + * @return the decoded string or null to indicate that more data is needed. + * @throws ArithmeticException if the string length value overflows a int. + * @throws EncodingException if the string encoding is invalid. + */ public String decode(ByteBuffer buffer) throws EncodingException { while (true) @@ -58,11 +84,11 @@ public class NBitStringParser byte firstByte = buffer.get(buffer.position()); _huffman = ((0x80 >>> (8 - _prefix)) & firstByte) != 0; _state = State.LENGTH; - _integerParser.setPrefix(_prefix - 1); + _integerDecoder.setPrefix(_prefix - 1); continue; case LENGTH: - _length = _integerParser.decodeInt(buffer); + _length = _integerDecoder.decodeInt(buffer); if (_length < 0) return null; _state = State.VALUE; @@ -70,7 +96,7 @@ public class NBitStringParser continue; case VALUE: - String value = _huffman ? _huffmanBuilder.decode(buffer) : asciiStringDecode(buffer); + String value = _huffman ? _huffmanBuilder.decode(buffer) : stringDecode(buffer); if (value != null) reset(); return value; @@ -81,23 +107,24 @@ public class NBitStringParser } } - private String asciiStringDecode(ByteBuffer buffer) + private String stringDecode(ByteBuffer buffer) { for (; _count < _length; _count++) { if (!buffer.hasRemaining()) return null; - _stringBuilder.append((char)(0x7F & buffer.get())); + _builder.append(buffer.get()); } - return _stringBuilder.toString(); + + return _builder.build(); } public void reset() { _state = State.PARSING; - _integerParser.reset(); + _integerDecoder.reset(); _huffmanBuilder.reset(); - _stringBuilder.setLength(0); + _builder.reset(); _prefix = 0; _count = 0; _length = 0; diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java new file mode 100644 index 00000000000..42dd39981e9 --- /dev/null +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java @@ -0,0 +1,164 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class HuffmanTest +{ + public static String decode(ByteBuffer buffer, int length) throws EncodingException + { + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(length); + String decoded = huffmanDecoder.decode(buffer); + if (decoded == null) + throw new EncodingException("invalid string encoding"); + + huffmanDecoder.reset(); + return decoded; + } + + public static Stream data() + { + return Stream.of( + new String[][]{ + {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, + {"D.4.2", "a8eb10649cbf", "no-cache"}, + {"D.6.1k", "6402", "302"}, + {"D.6.1v", "aec3771a4b", "private"}, + {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, + {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, + {"D.6.2te", "640cff", "303"}, + }).map(Arguments::of); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + public void testDecode(String specSection, String hex, String expected) throws Exception + { + byte[] encoded = TypeUtil.fromHexString(hex); + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(encoded.length); + String decoded = huffmanDecoder.decode(ByteBuffer.wrap(encoded)); + assertEquals(expected, decoded, specSection); + } + + @ParameterizedTest(name = "[{index}] spec={0}") + @MethodSource("data") + public void testEncode(String specSection, String hex, String expected) + { + ByteBuffer buf = BufferUtil.allocate(1024); + int pos = BufferUtil.flipToFill(buf); + HuffmanEncoder.encode(buf, expected); + BufferUtil.flipToFlush(buf, pos); + String encoded = TypeUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH); + assertEquals(hex, encoded, specSection); + assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected)); + } + + public static Stream testDecode8859OnlyArguments() + { + return Stream.of( + // These are valid characters for ISO-8859-1. + Arguments.of("FfFe6f", (char)128), + Arguments.of("FfFfFbBf", (char)255), + + // RFC9110 specifies these to be replaced as ' ' during decoding. + Arguments.of("FfC7", ' '), // (char)0 + Arguments.of("FfFfFfF7", ' '), // '\r' + Arguments.of("FfFfFfF3", ' '), // '\n' + + // We replace control chars with the default replacement character of '?'. + Arguments.of("FfFfFfBf", '?') // (char)(' ' - 1) + ); + } + + @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name + @MethodSource("testDecode8859OnlyArguments") + public void testDecode8859Only(String hexString, char expected) throws Exception + { + ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(hexString)); + String decoded = decode(buffer, buffer.remaining()); + assertThat(decoded, equalTo("" + expected)); + } + + public static Stream testEncode8859OnlyArguments() + { + return Stream.of( + Arguments.of((char)128, (char)128), + Arguments.of((char)255, (char)255), + Arguments.of((char)0, null), + Arguments.of('\r', null), + Arguments.of('\n', null), + Arguments.of((char)456, null), + Arguments.of((char)256, null), + Arguments.of((char)-1, null), + Arguments.of((char)(' ' - 1), null) + ); + } + + @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name + @MethodSource("testEncode8859OnlyArguments") + public void testEncode8859Only(char value, Character expectedValue) throws Exception + { + String s = "value = '" + value + "'"; + + // If expected is null we should not be able to encode. + if (expectedValue == null) + { + assertThat(HuffmanEncoder.octetsNeeded(s), equalTo(-1)); + assertThrows(Throwable.class, () -> encode(s)); + return; + } + + String expected = "value = '" + expectedValue + "'"; + assertThat(HuffmanEncoder.octetsNeeded(s), greaterThan(0)); + ByteBuffer buffer = encode(s); + String decode = decode(buffer); + System.err.println("decoded: " + decode); + assertThat(decode, equalTo(expected)); + } + + private ByteBuffer encode(String s) + { + ByteBuffer buffer = BufferUtil.allocate(32); + BufferUtil.clearToFill(buffer); + HuffmanEncoder.encode(buffer, s); + BufferUtil.flipToFlush(buffer, 0); + return buffer; + } + + private String decode(ByteBuffer buffer) throws Exception + { + return decode(buffer, buffer.remaining()); + } +} diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerParserTest.java similarity index 81% rename from jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java rename to jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerParserTest.java index 249bf785a25..bd1969373eb 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerParserTest.java @@ -11,11 +11,11 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http; import java.nio.ByteBuffer; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.Test; @@ -29,7 +29,7 @@ public class NBitIntegerParserTest @Test public void testParsingOverByteBoundary() { - NBitIntegerParser parser = new NBitIntegerParser(); + NBitIntegerDecoder decoder = new NBitIntegerDecoder(); String encoded = "FFBA09"; byte[] bytes = TypeUtil.fromHexString(encoded); @@ -37,11 +37,11 @@ public class NBitIntegerParserTest ByteBuffer buffer1 = BufferUtil.toBuffer(bytes, 0, 2); ByteBuffer buffer2 = BufferUtil.toBuffer(bytes, 2, 1); - parser.setPrefix(7); - int value = parser.decodeInt(buffer1); + decoder.setPrefix(7); + int value = decoder.decodeInt(buffer1); assertThat(value, is(-1)); - value = parser.decodeInt(buffer2); + value = decoder.decodeInt(buffer2); assertThat(value, is(1337)); } } diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java similarity index 80% rename from jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerTest.java rename to jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java index 99f84265566..c390db1869c 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java @@ -11,12 +11,12 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http; import java.nio.ByteBuffer; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.Test; @@ -26,22 +26,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SuppressWarnings("PointlessArithmeticExpression") public class NBitIntegerTest { - private final NBitIntegerParser _parser = new NBitIntegerParser(); + private final NBitIntegerDecoder _decoder = new NBitIntegerDecoder(); @Test public void testOctetsNeeded() { - assertEquals(0, NBitIntegerEncoder.octectsNeeded(5, 10)); - assertEquals(2, NBitIntegerEncoder.octectsNeeded(5, 1337)); - assertEquals(1, NBitIntegerEncoder.octectsNeeded(8, 42)); - assertEquals(3, NBitIntegerEncoder.octectsNeeded(8, 1337)); + assertEquals(0, NBitIntegerEncoder.octetsNeeded(5, 10)); + assertEquals(2, NBitIntegerEncoder.octetsNeeded(5, 1337)); + assertEquals(1, NBitIntegerEncoder.octetsNeeded(8, 42)); + assertEquals(3, NBitIntegerEncoder.octetsNeeded(8, 1337)); - assertEquals(0, NBitIntegerEncoder.octectsNeeded(6, 62)); - assertEquals(1, NBitIntegerEncoder.octectsNeeded(6, 63)); - assertEquals(1, NBitIntegerEncoder.octectsNeeded(6, 64)); - assertEquals(2, NBitIntegerEncoder.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); - assertEquals(3, NBitIntegerEncoder.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); - assertEquals(4, NBitIntegerEncoder.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); + assertEquals(0, NBitIntegerEncoder.octetsNeeded(6, 62)); + assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 63)); + assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 64)); + assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); + assertEquals(3, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); + assertEquals(4, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); } @Test @@ -87,7 +87,7 @@ public class NBitIntegerTest String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); assertEquals(expected, r); - assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitIntegerEncoder.octectsNeeded(n, i)); + assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitIntegerEncoder.octetsNeeded(n, i)); } @Test @@ -125,8 +125,8 @@ public class NBitIntegerTest public void testDecode(int n, int expected, String encoded) { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); - _parser.setPrefix(n); - assertEquals(expected, _parser.decodeInt(buf)); + _decoder.setPrefix(n); + assertEquals(expected, _decoder.decodeInt(buf)); } @Test @@ -149,8 +149,8 @@ public class NBitIntegerTest { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("77EaFF")); buf.position(1); - _parser.setPrefix(5); - assertEquals(10, _parser.decodeInt(buf)); + _decoder.setPrefix(5); + assertEquals(10, _decoder.decodeInt(buf)); } @Test @@ -173,8 +173,8 @@ public class NBitIntegerTest { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("881f9a0aff")); buf.position(1); - _parser.setPrefix(5); - assertEquals(1337, _parser.decodeInt(buf)); + _decoder.setPrefix(5); + assertEquals(1337, _decoder.decodeInt(buf)); } @Test @@ -197,7 +197,7 @@ public class NBitIntegerTest { ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("882aFf")); buf.position(1); - _parser.setPrefix(8); - assertEquals(42, _parser.decodeInt(buf)); + _decoder.setPrefix(8); + assertEquals(42, _decoder.decodeInt(buf)); } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/AuthorityHttpField.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/AuthorityHttpField.java index eef7a229156..3dc58f56a72 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/AuthorityHttpField.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/AuthorityHttpField.java @@ -16,9 +16,6 @@ package org.eclipse.jetty.http2.hpack; import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpHeader; -/** - * - */ public class AuthorityHttpField extends HostPortHttpField { public static final String AUTHORITY = HpackContext.STATIC_TABLE[1][0]; diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java index 2917b7b4d0b..6a774a51f07 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java @@ -24,6 +24,8 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; @@ -457,19 +459,19 @@ public class HpackContext String value = field.getValue(); if (value != null && value.length() > 0) { - int huffmanLen = Huffman.octetsNeeded(value); + int huffmanLen = HuffmanEncoder.octetsNeeded(value); if (huffmanLen < 0) throw new IllegalStateException("bad value"); - int lenLen = NBitInteger.octectsNeeded(7, huffmanLen); + int lenLen = NBitIntegerEncoder.octetsNeeded(7, huffmanLen); _huffmanValue = new byte[1 + lenLen + huffmanLen]; ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); // Indicate Huffman buffer.put((byte)0x80); // Add huffman length - NBitInteger.encode(buffer, 7, huffmanLen); + NBitIntegerEncoder.encode(buffer, 7, huffmanLen); // Encode value - Huffman.encode(buffer, value); + HuffmanEncoder.encode(buffer, value); } else _huffmanValue = null; diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java index 4367eca1d31..577caabbb47 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java @@ -19,8 +19,12 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTokens; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.CharsetStringBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +40,8 @@ public class HpackDecoder private final HpackContext _context; private final MetaDataBuilder _builder; + private final HuffmanDecoder _huffmanDecoder; + private final NBitIntegerDecoder _integerDecoder; private int _localMaxDynamicTableSize; /** @@ -47,6 +53,8 @@ public class HpackDecoder _context = new HpackContext(localMaxDynamicTableSize); _localMaxDynamicTableSize = localMaxDynamicTableSize; _builder = new MetaDataBuilder(maxHeaderSize); + _huffmanDecoder = new HuffmanDecoder(); + _integerDecoder = new NBitIntegerDecoder(); } public HpackContext getHpackContext() @@ -64,7 +72,8 @@ public class HpackDecoder if (LOG.isDebugEnabled()) LOG.debug(String.format("CtxTbl[%x] decoding %d octets", _context.hashCode(), buffer.remaining())); - // If the buffer is big, don't even think about decoding it + // If the buffer is big, don't even think about decoding it. + // Huffman may double the size, but it will only be a temporary allocation until detected in MetaDataBuilder.emit(). if (buffer.remaining() > _builder.getMaxSize()) throw new HpackException.SessionException("431 Request Header Fields too large"); @@ -79,7 +88,7 @@ public class HpackDecoder if (b < 0) { // 7.1 indexed if the high bit is set - int index = NBitInteger.decode(buffer, 7); + int index = integerDecode(buffer, 7); Entry entry = _context.get(index); if (entry == null) throw new HpackException.SessionException("Unknown index %d", index); @@ -120,7 +129,7 @@ public class HpackDecoder case 2: // 7.3 case 3: // 7.3 // change table size - int size = NBitInteger.decode(buffer, 5); + int size = integerDecode(buffer, 5); if (LOG.isDebugEnabled()) LOG.debug("decode resize={}", size); if (size > _localMaxDynamicTableSize) @@ -133,7 +142,7 @@ public class HpackDecoder case 0: // 7.2.2 case 1: // 7.2.3 indexed = false; - nameIndex = NBitInteger.decode(buffer, 4); + nameIndex = integerDecode(buffer, 4); break; case 4: // 7.2.1 @@ -141,7 +150,7 @@ public class HpackDecoder case 6: // 7.2.1 case 7: // 7.2.1 indexed = true; - nameIndex = NBitInteger.decode(buffer, 6); + nameIndex = integerDecode(buffer, 6); break; default: @@ -160,12 +169,11 @@ public class HpackDecoder else { huffmanName = (buffer.get() & 0x80) == 0x80; - int length = NBitInteger.decode(buffer, 7); - _builder.checkSize(length, huffmanName); + int length = integerDecode(buffer, 7); if (huffmanName) - name = Huffman.decode(buffer, length); + name = huffmanDecode(buffer, length); else - name = toASCIIString(buffer, length); + name = toISO88591String(buffer, length); check: for (int i = name.length(); i-- > 0; ) { @@ -201,12 +209,11 @@ public class HpackDecoder // decode the value boolean huffmanValue = (buffer.get() & 0x80) == 0x80; - int length = NBitInteger.decode(buffer, 7); - _builder.checkSize(length, huffmanValue); + int length = integerDecode(buffer, 7); if (huffmanValue) - value = Huffman.decode(buffer, length); + value = huffmanDecode(buffer, length); else - value = toASCIIString(buffer, length); + value = toISO88591String(buffer, length); // Make the new field HttpField field; @@ -267,14 +274,61 @@ public class HpackDecoder return _builder.build(); } - public static String toASCIIString(ByteBuffer buffer, int length) + private int integerDecode(ByteBuffer buffer, int prefix) throws HpackException.CompressionException { - StringBuilder builder = new StringBuilder(length); + try + { + if (prefix != 8) + buffer.position(buffer.position() - 1); + + _integerDecoder.setPrefix(prefix); + int decodedInt = _integerDecoder.decodeInt(buffer); + if (decodedInt < 0) + throw new EncodingException("invalid integer encoding"); + return decodedInt; + } + catch (EncodingException e) + { + HpackException.CompressionException compressionException = new HpackException.CompressionException(e.getMessage()); + compressionException.initCause(e); + throw compressionException; + } + finally + { + _integerDecoder.reset(); + } + } + + private String huffmanDecode(ByteBuffer buffer, int length) throws HpackException.CompressionException + { + try + { + _huffmanDecoder.setLength(length); + String decoded = _huffmanDecoder.decode(buffer); + if (decoded == null) + throw new HpackException.CompressionException("invalid string encoding"); + return decoded; + } + catch (EncodingException e) + { + HpackException.CompressionException compressionException = new HpackException.CompressionException(e.getMessage()); + compressionException.initCause(e); + throw compressionException; + } + finally + { + _huffmanDecoder.reset(); + } + } + + public static String toISO88591String(ByteBuffer buffer, int length) + { + CharsetStringBuilder.Iso88591StringBuilder builder = new CharsetStringBuilder.Iso88591StringBuilder(); for (int i = 0; i < length; ++i) { - builder.append((char)(0x7F & buffer.get())); + builder.append(HttpTokens.sanitizeFieldVchar((char)buffer.get())); } - return builder.toString(); + return builder.build(); } @Override diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java index 9d89b040e6d..e84bdb4e367 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; @@ -26,9 +25,12 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpTokens; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry; import org.eclipse.jetty.util.BufferUtil; @@ -286,7 +288,7 @@ public class HpackEncoder if (maxDynamicTableSize > _remoteMaxDynamicTableSize) throw new IllegalArgumentException(); buffer.put((byte)0x20); - NBitInteger.encode(buffer, 5, maxDynamicTableSize); + NBitIntegerEncoder.encode(buffer, 5, maxDynamicTableSize); _context.resize(maxDynamicTableSize); } @@ -315,9 +317,9 @@ public class HpackEncoder { int index = _context.index(entry); buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, index); + NBitIntegerEncoder.encode(buffer, 7, index); if (_debug) - encoding = "IdxField" + (entry.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(7, index)); + encoding = "IdxField" + (entry.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(7, index)); } } else @@ -391,19 +393,19 @@ public class HpackEncoder if (_debug) encoding = "Lit" + - ((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(4, _context.index(name))))) + + ((name == null) ? "HuffN" : ("IdxN" + (name.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(4, _context.index(name))))) + (huffman ? "HuffV" : "LitV") + (neverIndex ? "!!Idx" : "!Idx"); } else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && !"0".equals(field.getValue())) { - // The field is too large or a non zero content length, so do not index. + // The field is too large or a non-zero content length, so do not index. indexed = false; encodeName(buffer, (byte)0x00, 4, header.asString(), name); encodeValue(buffer, true, field.getValue()); if (_debug) encoding = "Lit" + - ((name == null) ? "HuffN" : "IdxNS" + (1 + NBitInteger.octectsNeeded(4, _context.index(name)))) + + ((name == null) ? "HuffN" : "IdxNS" + (1 + NBitIntegerEncoder.octetsNeeded(4, _context.index(name)))) + "HuffV!Idx"; } else @@ -414,7 +416,7 @@ public class HpackEncoder encodeName(buffer, (byte)0x40, 6, header.asString(), name); encodeValue(buffer, huffman, field.getValue()); if (_debug) - encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitInteger.octectsNeeded(6, _context.index(name))))) + + encoding = ((name == null) ? "LitHuffN" : ("LitIdxN" + (name.isStatic() ? "S" : "") + (1 + NBitIntegerEncoder.octetsNeeded(6, _context.index(name))))) + (huffman ? "HuffVIdx" : "LitVIdx"); } } @@ -439,12 +441,12 @@ public class HpackEncoder // leave name index bits as 0 // Encode the name always with lowercase huffman buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); - Huffman.encodeLC(buffer, name); + NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowerCase(name)); + HuffmanEncoder.encodeLowerCase(buffer, name); } else { - NBitInteger.encode(buffer, bits, _context.index(entry)); + NBitIntegerEncoder.encode(buffer, bits, _context.index(entry)); } } @@ -454,38 +456,19 @@ public class HpackEncoder { // huffman literal value buffer.put((byte)0x80); - - int needed = Huffman.octetsNeeded(value); - if (needed >= 0) - { - NBitInteger.encode(buffer, 7, needed); - Huffman.encode(buffer, value); - } - else - { - // Not iso_8859_1 - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(bytes)); - Huffman.encode(buffer, bytes); - } + int needed = HuffmanEncoder.octetsNeeded(value); + NBitIntegerEncoder.encode(buffer, 7, needed); + HuffmanEncoder.encode(buffer, value); } else { // add literal assuming iso_8859_1 buffer.put((byte)0x00).mark(); - NBitInteger.encode(buffer, 7, value.length()); + NBitIntegerEncoder.encode(buffer, 7, value.length()); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); - if (c < ' ' || c > 127) - { - // Not iso_8859_1, so re-encode as UTF-8 - buffer.reset(); - byte[] bytes = value.getBytes(StandardCharsets.UTF_8); - NBitInteger.encode(buffer, 7, bytes.length); - buffer.put(bytes, 0, bytes.length); - return; - } + c = HttpTokens.sanitizeFieldVchar(c); buffer.put((byte)c); } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java index 663bb24d3d6..60faded01aa 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java @@ -13,7 +13,6 @@ package org.eclipse.jetty.http2.hpack; -@SuppressWarnings("serial") public abstract class HpackException extends Exception { HpackException(String messageFormat, Object... args) @@ -30,7 +29,7 @@ public abstract class HpackException extends Exception */ public static class StreamException extends HpackException { - StreamException(String messageFormat, Object... args) + public StreamException(String messageFormat, Object... args) { super(messageFormat, args); } @@ -43,7 +42,7 @@ public abstract class HpackException extends Exception */ public static class SessionException extends HpackException { - SessionException(String messageFormat, Object... args) + public SessionException(String messageFormat, Object... args) { super(messageFormat, args); } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java index ae6b7e2d2b4..d3e5f7fddde 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java @@ -18,6 +18,8 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.http.HttpFieldPreEncoder; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.BufferUtil; /** @@ -67,12 +69,12 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder int nameIdx = HpackContext.staticIndex(header); if (nameIdx > 0) - NBitInteger.encode(buffer, bits, nameIdx); + NBitIntegerEncoder.encode(buffer, bits, nameIdx); else { buffer.put((byte)0x80); - NBitInteger.encode(buffer, 7, Huffman.octetsNeededLC(name)); - Huffman.encodeLC(buffer, name); + NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowerCase(name)); + HuffmanEncoder.encodeLowerCase(buffer, name); } HpackEncoder.encodeValue(buffer, huffman, value); diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java index 83314f0e268..b00be5c232e 100644 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java +++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java @@ -34,7 +34,7 @@ public class MetaDataBuilder private HostPortHttpField _authority; private String _path; private String _protocol; - private long _contentLength = Long.MIN_VALUE; + private long _contentLength = -1; private HpackException.StreamException _streamException; private boolean _request; private boolean _response; @@ -67,17 +67,17 @@ public class MetaDataBuilder return _size; } - public void emit(HttpField field) throws HpackException.SessionException + public void emit(HttpField field) throws SessionException { HttpHeader header = field.getHeader(); String name = field.getName(); if (name == null || name.length() == 0) - throw new HpackException.SessionException("Header size 0"); + throw new SessionException("Header size 0"); String value = field.getValue(); int fieldSize = name.length() + (value == null ? 0 : value.length()); _size += fieldSize + 32; if (_size > _maxSize) - throw new HpackException.SessionException("Header size %d > %d", _size, _maxSize); + throw new SessionException("Header size %d > %d", _size, _maxSize); if (field instanceof StaticTableHttpField) { @@ -196,7 +196,7 @@ public class MetaDataBuilder } } - protected void streamException(String messageFormat, Object... args) + public void streamException(String messageFormat, Object... args) { HpackException.StreamException stream = new HpackException.StreamException(messageFormat, args); if (_streamException == null) @@ -277,23 +277,7 @@ public class MetaDataBuilder _path = null; _protocol = null; _size = 0; - _contentLength = Long.MIN_VALUE; + _contentLength = -1; } } - - /** - * Check that the max size will not be exceeded. - * - * @param length the length - * @param huffman the huffman name - * @throws SessionException in case of size errors - */ - public void checkSize(int length, boolean huffman) throws SessionException - { - // Apply a huffman fudge factor - if (huffman) - length = (length * 4) / 3; - if ((_size + length) > _maxSize) - throw new HpackException.SessionException("Header too large %d > %d", _size + length, _maxSize); - } } diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java deleted file mode 100644 index 93a9fd4c688..00000000000 --- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java +++ /dev/null @@ -1,146 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.http2.hpack; - -import java.nio.ByteBuffer; - -public class NBitInteger -{ - public static int octectsNeeded(int n, int i) - { - if (n == 8) - { - int nbits = 0xFF; - i = i - nbits; - if (i < 0) - return 1; - if (i == 0) - return 2; - int lz = Integer.numberOfLeadingZeros(i); - int log = 32 - lz; - return 1 + (log + 6) / 7; - } - - int nbits = 0xFF >>> (8 - n); - i = i - nbits; - if (i < 0) - return 0; - if (i == 0) - return 1; - int lz = Integer.numberOfLeadingZeros(i); - int log = 32 - lz; - return (log + 6) / 7; - } - - public static void encode(ByteBuffer buf, int n, int i) - { - if (n == 8) - { - if (i < 0xFF) - { - buf.put((byte)i); - } - else - { - buf.put((byte)0xFF); - - int length = i - 0xFF; - while (true) - { - if ((length & ~0x7F) == 0) - { - buf.put((byte)length); - return; - } - else - { - buf.put((byte)((length & 0x7F) | 0x80)); - length >>>= 7; - } - } - } - } - else - { - int p = buf.position() - 1; - int bits = 0xFF >>> (8 - n); - - if (i < bits) - { - buf.put(p, (byte)((buf.get(p) & ~bits) | i)); - } - else - { - buf.put(p, (byte)(buf.get(p) | bits)); - - int length = i - bits; - while (true) - { - if ((length & ~0x7F) == 0) - { - buf.put((byte)length); - return; - } - else - { - buf.put((byte)((length & 0x7F) | 0x80)); - length >>>= 7; - } - } - } - } - } - - public static int decode(ByteBuffer buffer, int n) - { - if (n == 8) - { - int nbits = 0xFF; - - int i = buffer.get() & 0xff; - - if (i == nbits) - { - int m = 1; - int b; - do - { - b = 0xff & buffer.get(); - i = i + (b & 127) * m; - m = m * 128; - } - while ((b & 128) == 128); - } - return i; - } - - int nbits = 0xFF >>> (8 - n); - - int i = buffer.get(buffer.position() - 1) & nbits; - - if (i == nbits) - { - int m = 1; - int b; - do - { - b = 0xff & buffer.get(); - i = i + (b & 127) * m; - m = m * 128; - } - while ((b & 128) == 128); - } - return i; - } -} diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java index 96f7c6666e7..e2c5eddd110 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java @@ -16,6 +16,9 @@ package org.eclipse.jetty.http2.hpack; import java.nio.ByteBuffer; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.HuffmanDecoder; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -32,6 +35,32 @@ import static org.junit.jupiter.api.Assertions.assertTrue; */ public class HpackContextTest { + public static String decode(ByteBuffer buffer, int length) throws EncodingException + { + HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); + huffmanDecoder.setLength(length); + String decoded = huffmanDecoder.decode(buffer); + if (decoded == null) + throw new EncodingException("invalid string encoding"); + + huffmanDecoder.reset(); + return decoded; + } + + public static int decodeInt(ByteBuffer buffer, int prefix) throws EncodingException + { + // This is a fix for HPACK as it already takes the first byte of the encoded integer. + if (prefix != 8) + buffer.position(buffer.position() - 1); + + NBitIntegerDecoder decoder = new NBitIntegerDecoder(); + decoder.setPrefix(prefix); + int decodedInt = decoder.decodeInt(buffer); + if (decodedInt < 0) + throw new EncodingException("invalid integer encoding"); + decoder.reset(); + return decodedInt; + } @Test public void testStaticName() @@ -423,10 +452,10 @@ public class HpackContextTest int huff = 0xff & buffer.get(); assertTrue((0x80 & huff) == 0x80); - int len = NBitInteger.decode(buffer, 7); + int len = decodeInt(buffer, 7); assertEquals(len, buffer.remaining()); - String value = Huffman.decode(buffer); + String value = decode(buffer, buffer.remaining()); assertEquals(entry.getHttpField().getValue(), value); } diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java index 9f9a62081d2..3a0de9189eb 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java @@ -465,7 +465,7 @@ public class HpackDecoderTest String encoded = "82868441" + "84" + "49509FFF"; ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } /* 5.2.2: Sends a Huffman-encoded string literal representation padded by zero */ @@ -478,7 +478,7 @@ public class HpackDecoderTest ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Incorrect padding")); + assertThat(ex.getMessage(), Matchers.containsString("incorrect_padding")); } /* 5.2.3: Sends a Huffman-encoded string literal representation containing the EOS symbol */ @@ -491,7 +491,7 @@ public class HpackDecoderTest ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("EOS in content")); + assertThat(ex.getMessage(), Matchers.containsString("eos_in_content")); } @Test @@ -503,7 +503,7 @@ public class HpackDecoderTest ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } @Test @@ -515,7 +515,7 @@ public class HpackDecoderTest ByteBuffer buffer = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); CompressionException ex = assertThrows(CompressionException.class, () -> decoder.decode(buffer)); - assertThat(ex.getMessage(), Matchers.containsString("Bad termination")); + assertThat(ex.getMessage(), Matchers.containsString("bad_termination")); } @Test diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java index 576d310937d..8073a331c6a 100644 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java +++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java @@ -129,31 +129,32 @@ public class HpackTest } catch (HpackException.SessionException e) { - assertThat(e.getMessage(), containsString("Header too large")); + assertThat(e.getMessage(), containsString("Header size 198 > 164")); } } @Test - public void encodeDecodeNonAscii() throws Exception + public void encodeNonAscii() throws Exception { HpackEncoder encoder = new HpackEncoder(); - HpackDecoder decoder = new HpackDecoder(4096, 8192); ByteBuffer buffer = BufferUtil.allocate(16 * 1024); HttpFields fields0 = HttpFields.build() - // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck + // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck .add("Cookie", "[\uD842\uDF9F]") .add("custom-key", "[\uD842\uDF9F]"); Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0); - BufferUtil.clearToFill(buffer); - encoder.encode(buffer, original0); - BufferUtil.flipToFlush(buffer, 0); - Response decoded0 = (Response)decoder.decode(buffer); + HpackException.SessionException throwable = assertThrows(HpackException.SessionException.class, () -> + { + BufferUtil.clearToFill(buffer); + encoder.encode(buffer, original0); + BufferUtil.flipToFlush(buffer, 0); + }); - assertMetaDataSame(original0, decoded0); + assertThat(throwable.getMessage(), containsString("Could not hpack encode")); } - + @Test public void evictReferencedFieldTest() throws Exception { diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java deleted file mode 100644 index 72cc6fea983..00000000000 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java +++ /dev/null @@ -1,82 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.http2.hpack; - -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.util.Locale; -import java.util.stream.Stream; - -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.TypeUtil; -import org.hamcrest.Matchers; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class HuffmanTest -{ - public static Stream data() - { - return Stream.of( - new String[][]{ - {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, - {"D.4.2", "a8eb10649cbf", "no-cache"}, - {"D.6.1k", "6402", "302"}, - {"D.6.1v", "aec3771a4b", "private"}, - {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, - {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, - {"D.6.2te", "640cff", "303"}, - }).map(Arguments::of); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testDecode(String specSection, String hex, String expected) throws Exception - { - byte[] encoded = TypeUtil.fromHexString(hex); - String decoded = Huffman.decode(ByteBuffer.wrap(encoded)); - assertEquals(expected, decoded, specSection); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testEncode(String specSection, String hex, String expected) - { - ByteBuffer buf = BufferUtil.allocate(1024); - int pos = BufferUtil.flipToFill(buf); - Huffman.encode(buf, expected); - BufferUtil.flipToFlush(buf, pos); - String encoded = TypeUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH); - assertEquals(hex, encoded, specSection); - assertEquals(hex.length() / 2, Huffman.octetsNeeded(expected)); - } - - @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name - @ValueSource(chars = {(char)128, (char)0, (char)-1, ' ' - 1}) - public void testEncode8859Only(char bad) - { - String s = "bad '" + bad + "'"; - - assertThat(Huffman.octetsNeeded(s), Matchers.is(-1)); - - assertThrows(BufferOverflowException.class, - () -> Huffman.encode(BufferUtil.allocate(32), s)); - } -} diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java deleted file mode 100644 index 22b5acb2ce6..00000000000 --- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java +++ /dev/null @@ -1,199 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.http2.hpack; - -import java.nio.ByteBuffer; - -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.TypeUtil; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class NBitIntegerTest -{ - - @Test - public void testOctetsNeeded() - { - assertEquals(0, NBitInteger.octectsNeeded(5, 10)); - assertEquals(2, NBitInteger.octectsNeeded(5, 1337)); - assertEquals(1, NBitInteger.octectsNeeded(8, 42)); - assertEquals(3, NBitInteger.octectsNeeded(8, 1337)); - - assertEquals(0, NBitInteger.octectsNeeded(6, 62)); - assertEquals(1, NBitInteger.octectsNeeded(6, 63)); - assertEquals(1, NBitInteger.octectsNeeded(6, 64)); - assertEquals(2, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x01)); - assertEquals(3, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80)); - assertEquals(4, NBitInteger.octectsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80)); - } - - @Test - public void testEncode() - { - testEncode(6, 0, "00"); - testEncode(6, 1, "01"); - testEncode(6, 62, "3e"); - testEncode(6, 63, "3f00"); - testEncode(6, 63 + 1, "3f01"); - testEncode(6, 63 + 0x7e, "3f7e"); - testEncode(6, 63 + 0x7f, "3f7f"); - testEncode(6, 63 + 0x00 + 0x80 * 0x01, "3f8001"); - testEncode(6, 63 + 0x01 + 0x80 * 0x01, "3f8101"); - testEncode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01"); - testEncode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002"); - testEncode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102"); - testEncode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f"); - testEncode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001"); - testEncode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f"); - testEncode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001"); - - testEncode(8, 0, "00"); - testEncode(8, 1, "01"); - testEncode(8, 128, "80"); - testEncode(8, 254, "Fe"); - testEncode(8, 255, "Ff00"); - testEncode(8, 255 + 1, "Ff01"); - testEncode(8, 255 + 0x7e, "Ff7e"); - testEncode(8, 255 + 0x7f, "Ff7f"); - testEncode(8, 255 + 0x80, "Ff8001"); - testEncode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001"); - } - - public void testEncode(int n, int i, String expected) - { - ByteBuffer buf = BufferUtil.allocate(16); - int p = BufferUtil.flipToFill(buf); - if (n < 8) - buf.put((byte)0x00); - NBitInteger.encode(buf, n, i); - BufferUtil.flipToFlush(buf, p); - String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); - assertEquals(expected, r); - - assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitInteger.octectsNeeded(n, i)); - } - - @Test - public void testDecode() - { - testDecode(6, 0, "00"); - testDecode(6, 1, "01"); - testDecode(6, 62, "3e"); - testDecode(6, 63, "3f00"); - testDecode(6, 63 + 1, "3f01"); - testDecode(6, 63 + 0x7e, "3f7e"); - testDecode(6, 63 + 0x7f, "3f7f"); - testDecode(6, 63 + 0x80, "3f8001"); - testDecode(6, 63 + 0x81, "3f8101"); - testDecode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01"); - testDecode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002"); - testDecode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102"); - testDecode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f"); - testDecode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001"); - testDecode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f"); - testDecode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001"); - - testDecode(8, 0, "00"); - testDecode(8, 1, "01"); - testDecode(8, 128, "80"); - testDecode(8, 254, "Fe"); - testDecode(8, 255, "Ff00"); - testDecode(8, 255 + 1, "Ff01"); - testDecode(8, 255 + 0x7e, "Ff7e"); - testDecode(8, 255 + 0x7f, "Ff7f"); - testDecode(8, 255 + 0x80, "Ff8001"); - testDecode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001"); - } - - public void testDecode(int n, int expected, String encoded) - { - ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString(encoded)); - buf.position(n == 8 ? 0 : 1); - assertEquals(expected, NBitInteger.decode(buf, n)); - } - - @Test - public void testEncodeExampleD11() - { - ByteBuffer buf = BufferUtil.allocate(16); - int p = BufferUtil.flipToFill(buf); - buf.put((byte)0x77); - buf.put((byte)0xFF); - NBitInteger.encode(buf, 5, 10); - BufferUtil.flipToFlush(buf, p); - - String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); - - assertEquals("77Ea", r); - } - - @Test - public void testDecodeExampleD11() - { - ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("77EaFF")); - buf.position(2); - - assertEquals(10, NBitInteger.decode(buf, 5)); - } - - @Test - public void testEncodeExampleD12() - { - ByteBuffer buf = BufferUtil.allocate(16); - int p = BufferUtil.flipToFill(buf); - buf.put((byte)0x88); - buf.put((byte)0x00); - NBitInteger.encode(buf, 5, 1337); - BufferUtil.flipToFlush(buf, p); - - String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); - - assertEquals("881f9a0a", r); - } - - @Test - public void testDecodeExampleD12() - { - ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("881f9a0aff")); - buf.position(2); - - assertEquals(1337, NBitInteger.decode(buf, 5)); - } - - @Test - public void testEncodeExampleD13() - { - ByteBuffer buf = BufferUtil.allocate(16); - int p = BufferUtil.flipToFill(buf); - buf.put((byte)0x88); - buf.put((byte)0xFF); - NBitInteger.encode(buf, 8, 42); - BufferUtil.flipToFlush(buf, p); - - String r = TypeUtil.toHexString(BufferUtil.toArray(buf)); - - assertEquals("88Ff2a", r); - } - - @Test - public void testDecodeExampleD13() - { - ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("882aFf")); - buf.position(1); - - assertEquals(42, NBitInteger.decode(buf, 8)); - } -} diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java index 13fd93aac33..d01d59bf6b7 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackDecoder.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http3.qpack.internal.QpackContext; import org.eclipse.jetty.http3.qpack.internal.instruction.InsertCountIncrementInstruction; import org.eclipse.jetty.http3.qpack.internal.instruction.SectionAcknowledgmentInstruction; @@ -33,7 +34,6 @@ import org.eclipse.jetty.http3.qpack.internal.parser.EncodedFieldSection; import org.eclipse.jetty.http3.qpack.internal.table.DynamicTable; import org.eclipse.jetty.http3.qpack.internal.table.Entry; import org.eclipse.jetty.http3.qpack.internal.table.StaticTable; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.component.Dumpable; import org.slf4j.Logger; @@ -52,7 +52,7 @@ public class QpackDecoder implements Dumpable private final QpackContext _context; private final DecoderInstructionParser _parser; private final List _encodedFieldSections = new ArrayList<>(); - private final NBitIntegerParser _integerDecoder = new NBitIntegerParser(); + private final NBitIntegerDecoder _integerDecoder = new NBitIntegerDecoder(); private final InstructionHandler _instructionHandler = new InstructionHandler(); private final Map _blockedStreams = new HashMap<>(); private int _maxHeaderSize; @@ -136,6 +136,7 @@ public class QpackDecoder implements Dumpable LOG.debug("Decoding: streamId={}, buffer={}", streamId, BufferUtil.toDetailString(buffer)); // If the buffer is big, don't even think about decoding it + // Huffman may double the size, but it will only be a temporary allocation until detected in MetaDataBuilder.emit(). int maxHeaderSize = getMaxHeaderSize(); if (buffer.remaining() > maxHeaderSize) throw new QpackException.SessionException(QPACK_DECOMPRESSION_FAILED, "header_too_large"); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java index 0c0c4411504..8ce7cc5c32b 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java @@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.internal.EncodableEntry; import org.eclipse.jetty.http3.qpack.internal.QpackContext; import org.eclipse.jetty.http3.qpack.internal.StreamInfo; @@ -37,7 +38,6 @@ import org.eclipse.jetty.http3.qpack.internal.metadata.Http3Fields; import org.eclipse.jetty.http3.qpack.internal.parser.EncoderInstructionParser; import org.eclipse.jetty.http3.qpack.internal.table.DynamicTable; import org.eclipse.jetty.http3.qpack.internal.table.Entry; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.thread.AutoLock; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/EncodableEntry.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/EncodableEntry.java index c8abe4688ba..4d354b43544 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/EncodableEntry.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/EncodableEntry.java @@ -14,14 +14,15 @@ package org.eclipse.jetty.http3.qpack.internal; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Objects; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.PreEncodedHttpField; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.internal.table.Entry; -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanEncoder; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; public abstract class EncodableEntry { @@ -95,19 +96,19 @@ public abstract class EncodableEntry { // Indexed Field Line with Static Reference. int relativeIndex = _entry.getIndex(); - return 1 + NBitIntegerEncoder.octectsNeeded(6, relativeIndex); + return 1 + NBitIntegerEncoder.octetsNeeded(6, relativeIndex); } else if (_entry.getIndex() < base) { // Indexed Field Line with Dynamic Reference. int relativeIndex = base - (_entry.getIndex() + 1); - return 1 + NBitIntegerEncoder.octectsNeeded(6, relativeIndex); + return 1 + NBitIntegerEncoder.octetsNeeded(6, relativeIndex); } else { // Indexed Field Line with Post-Base Index. int relativeIndex = _entry.getIndex() - base; - return 1 + NBitIntegerEncoder.octectsNeeded(4, relativeIndex); + return 1 + NBitIntegerEncoder.octetsNeeded(4, relativeIndex); } } @@ -173,7 +174,7 @@ public abstract class EncodableEntry { buffer.put((byte)0x00); NBitIntegerEncoder.encode(buffer, 7, value.length()); - buffer.put(value.getBytes()); + buffer.put(value.getBytes(StandardCharsets.ISO_8859_1)); } } @@ -181,9 +182,26 @@ public abstract class EncodableEntry public int getRequiredSize(int base) { String value = getValue(); - int relativeIndex = _nameEntry.getIndex() - base; int valueLength = _huffman ? HuffmanEncoder.octetsNeeded(value) : value.length(); - return 1 + NBitIntegerEncoder.octectsNeeded(4, relativeIndex) + 1 + NBitIntegerEncoder.octectsNeeded(7, valueLength) + valueLength; + + int nameOctets; + if (_nameEntry.isStatic()) + { + int relativeIndex = _nameEntry.getIndex(); + nameOctets = NBitIntegerEncoder.octetsNeeded(4, relativeIndex); + } + else if (_nameEntry.getIndex() < base) + { + int relativeIndex = base - (_nameEntry.getIndex() + 1); + nameOctets = NBitIntegerEncoder.octetsNeeded(4, relativeIndex); + } + else + { + int relativeIndex = _nameEntry.getIndex() - base; + nameOctets = NBitIntegerEncoder.octetsNeeded(3, relativeIndex); + } + + return 1 + nameOctets + 1 + NBitIntegerEncoder.octetsNeeded(7, valueLength) + valueLength; } @Override @@ -229,13 +247,12 @@ public abstract class EncodableEntry } else { - // TODO: What charset should we be using? (this applies to the instruction generators as well). buffer.put((byte)(0x20 | allowIntermediary)); NBitIntegerEncoder.encode(buffer, 3, name.length()); - buffer.put(name.getBytes()); + buffer.put(name.getBytes(StandardCharsets.ISO_8859_1)); buffer.put((byte)0x00); NBitIntegerEncoder.encode(buffer, 7, value.length()); - buffer.put(value.getBytes()); + buffer.put(value.getBytes(StandardCharsets.ISO_8859_1)); } } @@ -246,7 +263,7 @@ public abstract class EncodableEntry String value = getValue(); int nameLength = _huffman ? HuffmanEncoder.octetsNeeded(name) : name.length(); int valueLength = _huffman ? HuffmanEncoder.octetsNeeded(value) : value.length(); - return 2 + NBitIntegerEncoder.octectsNeeded(3, nameLength) + nameLength + NBitIntegerEncoder.octectsNeeded(7, valueLength) + valueLength; + return 2 + NBitIntegerEncoder.octetsNeeded(3, nameLength) + nameLength + NBitIntegerEncoder.octetsNeeded(7, valueLength) + valueLength; } @Override @@ -268,7 +285,6 @@ public abstract class EncodableEntry } } - // TODO: pass in the HTTP version to avoid hard coding HTTP3? private static class PreEncodedEntry extends EncodableEntry { private final PreEncodedHttpField _httpField; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/DuplicateInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/DuplicateInstruction.java index 286a57a7c26..8ec1cb827e8 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/DuplicateInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/DuplicateInstruction.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -37,7 +37,7 @@ public class DuplicateInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(5, _index) + 1; + int size = NBitIntegerEncoder.octetsNeeded(5, _index) + 1; ByteBuffer buffer = lease.acquire(size, false); buffer.put((byte)0x00); NBitIntegerEncoder.encode(buffer, 5, _index); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/IndexedNameEntryInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/IndexedNameEntryInstruction.java index fccb7824bd4..555703192b1 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/IndexedNameEntryInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/IndexedNameEntryInstruction.java @@ -14,10 +14,11 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanEncoder; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -54,7 +55,7 @@ public class IndexedNameEntryInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(6, _index) + (_huffman ? HuffmanEncoder.octetsNeeded(_value) : _value.length()) + 2; + int size = NBitIntegerEncoder.octetsNeeded(6, _index) + (_huffman ? HuffmanEncoder.octetsNeeded(_value) : _value.length()) + 2; ByteBuffer buffer = lease.acquire(size, false); // First bit indicates the instruction, second bit is whether it is a dynamic table reference or not. @@ -72,7 +73,7 @@ public class IndexedNameEntryInstruction implements Instruction { buffer.put((byte)(0x00)); NBitIntegerEncoder.encode(buffer, 7, _value.length()); - buffer.put(_value.getBytes()); + buffer.put(_value.getBytes(StandardCharsets.ISO_8859_1)); } BufferUtil.flipToFlush(buffer, 0); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/InsertCountIncrementInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/InsertCountIncrementInstruction.java index a41818282b5..5d883a8572e 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/InsertCountIncrementInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/InsertCountIncrementInstruction.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -37,7 +37,7 @@ public class InsertCountIncrementInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(6, _increment) + 1; + int size = NBitIntegerEncoder.octetsNeeded(6, _increment) + 1; ByteBuffer buffer = lease.acquire(size, false); buffer.put((byte)0x00); NBitIntegerEncoder.encode(buffer, 6, _increment); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/LiteralNameEntryInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/LiteralNameEntryInstruction.java index dfa5ba4909c..79947c0a43f 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/LiteralNameEntryInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/LiteralNameEntryInstruction.java @@ -14,11 +14,12 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanEncoder; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -69,7 +70,7 @@ public class LiteralNameEntryInstruction implements Instruction { buffer.put((byte)(0x40)); NBitIntegerEncoder.encode(buffer, 5, _name.length()); - buffer.put(_name.getBytes()); + buffer.put(_name.getBytes(StandardCharsets.ISO_8859_1)); } if (_huffmanValue) @@ -81,8 +82,8 @@ public class LiteralNameEntryInstruction implements Instruction else { buffer.put((byte)(0x00)); - NBitIntegerEncoder.encode(buffer, 5, _value.length()); - buffer.put(_value.getBytes()); + NBitIntegerEncoder.encode(buffer, 7, _value.length()); + buffer.put(_value.getBytes(StandardCharsets.ISO_8859_1)); } BufferUtil.flipToFlush(buffer, 0); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SectionAcknowledgmentInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SectionAcknowledgmentInstruction.java index cedb01f1fad..efb107f32c6 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SectionAcknowledgmentInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SectionAcknowledgmentInstruction.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -37,7 +37,7 @@ public class SectionAcknowledgmentInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(7, _streamId) + 1; + int size = NBitIntegerEncoder.octetsNeeded(7, _streamId) + 1; ByteBuffer buffer = lease.acquire(size, false); buffer.put((byte)0x80); NBitIntegerEncoder.encode(buffer, 7, _streamId); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SetCapacityInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SetCapacityInstruction.java index a91511f8e8d..24eb0f44611 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SetCapacityInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/SetCapacityInstruction.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -37,7 +37,7 @@ public class SetCapacityInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(5, _capacity) + 1; + int size = NBitIntegerEncoder.octetsNeeded(5, _capacity) + 1; ByteBuffer buffer = lease.acquire(size, false); buffer.put((byte)0x20); NBitIntegerEncoder.encode(buffer, 5, _capacity); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/StreamCancellationInstruction.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/StreamCancellationInstruction.java index 10f976e6049..5dcf89f2bfd 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/StreamCancellationInstruction.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/instruction/StreamCancellationInstruction.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.instruction; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http3.qpack.Instruction; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; @@ -32,7 +32,7 @@ public class StreamCancellationInstruction implements Instruction @Override public void encode(ByteBufferPool.Lease lease) { - int size = NBitIntegerEncoder.octectsNeeded(6, _streamId) + 1; + int size = NBitIntegerEncoder.octetsNeeded(6, _streamId) + 1; ByteBuffer buffer = lease.acquire(size, false); buffer.put((byte)0x40); NBitIntegerEncoder.encode(buffer, 6, _streamId); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/DecoderInstructionParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/DecoderInstructionParser.java index 45c02fdd1a3..dc3a227fa81 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/DecoderInstructionParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/DecoderInstructionParser.java @@ -15,10 +15,10 @@ package org.eclipse.jetty.http3.qpack.internal.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; +import org.eclipse.jetty.http.compression.NBitStringDecoder; import org.eclipse.jetty.http3.qpack.QpackException; -import org.eclipse.jetty.http3.qpack.internal.util.EncodingException; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; -import org.eclipse.jetty.http3.qpack.internal.util.NBitStringParser; /** * Parses a stream of unframed instructions for the Decoder. These instructions are sent from the remote Encoder. @@ -26,8 +26,8 @@ import org.eclipse.jetty.http3.qpack.internal.util.NBitStringParser; public class DecoderInstructionParser { private final Handler _handler; - private final NBitStringParser _stringParser; - private final NBitIntegerParser _integerParser; + private final NBitStringDecoder _stringDecoder; + private final NBitIntegerDecoder _integerDecoder; private State _state = State.PARSING; private Operation _operation = Operation.NONE; @@ -66,8 +66,8 @@ public class DecoderInstructionParser public DecoderInstructionParser(Handler handler) { _handler = handler; - _stringParser = new NBitStringParser(); - _integerParser = new NBitIntegerParser(); + _stringDecoder = new NBitStringDecoder(); + _integerDecoder = new NBitIntegerDecoder(); } public void parse(ByteBuffer buffer) throws QpackException, EncodingException @@ -92,13 +92,13 @@ public class DecoderInstructionParser else if ((firstByte & 0x20) != 0) { _state = State.SET_CAPACITY; - _integerParser.setPrefix(5); + _integerDecoder.setPrefix(5); parseSetDynamicTableCapacity(buffer); } else { _state = State.DUPLICATE; - _integerParser.setPrefix(5); + _integerDecoder.setPrefix(5); parseDuplicate(buffer); } break; @@ -134,20 +134,20 @@ public class DecoderInstructionParser byte firstByte = buffer.get(buffer.position()); _referenceDynamicTable = (firstByte & 0x40) == 0; _operation = Operation.INDEX; - _integerParser.setPrefix(6); + _integerDecoder.setPrefix(6); continue; case INDEX: - _index = _integerParser.decodeInt(buffer); + _index = _integerDecoder.decodeInt(buffer); if (_index < 0) return; _operation = Operation.VALUE; - _stringParser.setPrefix(8); + _stringDecoder.setPrefix(8); continue; case VALUE: - String value = _stringParser.decode(buffer); + String value = _stringDecoder.decode(buffer); if (value == null) return; @@ -171,20 +171,20 @@ public class DecoderInstructionParser { case NONE: _operation = Operation.NAME; - _stringParser.setPrefix(6); + _stringDecoder.setPrefix(6); continue; case NAME: - _name = _stringParser.decode(buffer); + _name = _stringDecoder.decode(buffer); if (_name == null) return; _operation = Operation.VALUE; - _stringParser.setPrefix(8); + _stringDecoder.setPrefix(8); continue; case VALUE: - String value = _stringParser.decode(buffer); + String value = _stringDecoder.decode(buffer); if (value == null) return; @@ -201,7 +201,7 @@ public class DecoderInstructionParser private void parseDuplicate(ByteBuffer buffer) throws QpackException { - int index = _integerParser.decodeInt(buffer); + int index = _integerDecoder.decodeInt(buffer); if (index >= 0) { reset(); @@ -211,7 +211,7 @@ public class DecoderInstructionParser private void parseSetDynamicTableCapacity(ByteBuffer buffer) throws QpackException { - int capacity = _integerParser.decodeInt(buffer); + int capacity = _integerDecoder.decodeInt(buffer); if (capacity >= 0) { reset(); @@ -221,8 +221,8 @@ public class DecoderInstructionParser public void reset() { - _stringParser.reset(); - _integerParser.reset(); + _stringDecoder.reset(); + _integerDecoder.reset(); _state = State.PARSING; _operation = Operation.NONE; _referenceDynamicTable = false; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncodedFieldSection.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncodedFieldSection.java index 50023cc9ebb..ddcb939dacf 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncodedFieldSection.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncodedFieldSection.java @@ -19,13 +19,13 @@ import java.util.List; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.compression.EncodingException; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; +import org.eclipse.jetty.http.compression.NBitStringDecoder; import org.eclipse.jetty.http3.qpack.QpackDecoder; import org.eclipse.jetty.http3.qpack.QpackException; import org.eclipse.jetty.http3.qpack.internal.QpackContext; import org.eclipse.jetty.http3.qpack.internal.metadata.MetaDataBuilder; -import org.eclipse.jetty.http3.qpack.internal.util.EncodingException; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; -import org.eclipse.jetty.http3.qpack.internal.util.NBitStringParser; import org.eclipse.jetty.util.BufferUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,8 +36,8 @@ public class EncodedFieldSection { private static final Logger LOG = LoggerFactory.getLogger(EncodedFieldSection.class); - private final NBitIntegerParser _integerParser = new NBitIntegerParser(); - private final NBitStringParser _stringParser = new NBitStringParser(); + private final NBitIntegerDecoder _integerDecoder = new NBitIntegerDecoder(); + private final NBitStringDecoder _stringDecoder = new NBitStringDecoder(); private final List _encodedFields = new ArrayList<>(); private final long _streamId; @@ -111,8 +111,8 @@ public class EncodedFieldSection { byte firstByte = buffer.get(buffer.position()); boolean dynamicTable = (firstByte & 0x40) == 0; - _integerParser.setPrefix(6); - int index = _integerParser.decodeInt(buffer); + _integerDecoder.setPrefix(6); + int index = _integerDecoder.decodeInt(buffer); if (index < 0) throw new EncodingException("invalid_index"); return new IndexedField(dynamicTable, index); @@ -120,8 +120,8 @@ public class EncodedFieldSection private EncodedField parseIndexedFieldPostBase(ByteBuffer buffer) throws EncodingException { - _integerParser.setPrefix(4); - int index = _integerParser.decodeInt(buffer); + _integerDecoder.setPrefix(4); + int index = _integerDecoder.decodeInt(buffer); if (index < 0) throw new EncodingException("Invalid Index"); @@ -137,13 +137,13 @@ public class EncodedFieldSection boolean allowEncoding = (firstByte & 0x20) != 0; boolean dynamicTable = (firstByte & 0x10) == 0; - _integerParser.setPrefix(4); - int nameIndex = _integerParser.decodeInt(buffer); + _integerDecoder.setPrefix(4); + int nameIndex = _integerDecoder.decodeInt(buffer); if (nameIndex < 0) throw new EncodingException("invalid_name_index"); - _stringParser.setPrefix(8); - String value = _stringParser.decode(buffer); + _stringDecoder.setPrefix(8); + String value = _stringDecoder.decode(buffer); if (value == null) throw new EncodingException("incomplete_value"); @@ -155,13 +155,13 @@ public class EncodedFieldSection byte firstByte = buffer.get(buffer.position()); boolean allowEncoding = (firstByte & 0x08) != 0; - _integerParser.setPrefix(3); - int nameIndex = _integerParser.decodeInt(buffer); + _integerDecoder.setPrefix(3); + int nameIndex = _integerDecoder.decodeInt(buffer); if (nameIndex < 0) throw new EncodingException("invalid_index"); - _stringParser.setPrefix(8); - String value = _stringParser.decode(buffer); + _stringDecoder.setPrefix(8); + String value = _stringDecoder.decode(buffer); if (value == null) throw new EncodingException("invalid_value"); @@ -173,13 +173,13 @@ public class EncodedFieldSection byte firstByte = buffer.get(buffer.position()); boolean allowEncoding = (firstByte & 0x10) != 0; - _stringParser.setPrefix(4); - String name = _stringParser.decode(buffer); + _stringDecoder.setPrefix(4); + String name = _stringDecoder.decode(buffer); if (name == null) throw new EncodingException("invalid_name"); - _stringParser.setPrefix(8); - String value = _stringParser.decode(buffer); + _stringDecoder.setPrefix(8); + String value = _stringDecoder.decode(buffer); if (value == null) throw new EncodingException("invalid_value"); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncoderInstructionParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncoderInstructionParser.java index 48a17eb19bd..87f56cfc86d 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncoderInstructionParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/parser/EncoderInstructionParser.java @@ -15,8 +15,8 @@ package org.eclipse.jetty.http3.qpack.internal.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.compression.NBitIntegerDecoder; import org.eclipse.jetty.http3.qpack.QpackException; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerParser; /** * Parses a stream of unframed instructions for the Encoder. These instructions are sent from the remote Decoder. @@ -28,7 +28,7 @@ public class EncoderInstructionParser private static final int INSERT_COUNT_INCREMENT_PREFIX = 6; private final Handler _handler; - private final NBitIntegerParser _integerParser; + private final NBitIntegerDecoder _integerDecoder; private State _state = State.IDLE; private enum State @@ -51,7 +51,7 @@ public class EncoderInstructionParser public EncoderInstructionParser(Handler handler) { _handler = handler; - _integerParser = new NBitIntegerParser(); + _integerDecoder = new NBitIntegerDecoder(); } public void parse(ByteBuffer buffer) throws QpackException @@ -67,19 +67,19 @@ public class EncoderInstructionParser if ((firstByte & 0x80) != 0) { _state = State.SECTION_ACKNOWLEDGEMENT; - _integerParser.setPrefix(SECTION_ACKNOWLEDGEMENT_PREFIX); + _integerDecoder.setPrefix(SECTION_ACKNOWLEDGEMENT_PREFIX); parseSectionAcknowledgment(buffer); } else if ((firstByte & 0x40) != 0) { _state = State.STREAM_CANCELLATION; - _integerParser.setPrefix(STREAM_CANCELLATION_PREFIX); + _integerDecoder.setPrefix(STREAM_CANCELLATION_PREFIX); parseStreamCancellation(buffer); } else { _state = State.INSERT_COUNT_INCREMENT; - _integerParser.setPrefix(INSERT_COUNT_INCREMENT_PREFIX); + _integerDecoder.setPrefix(INSERT_COUNT_INCREMENT_PREFIX); parseInsertCountIncrement(buffer); } break; @@ -103,7 +103,7 @@ public class EncoderInstructionParser private void parseSectionAcknowledgment(ByteBuffer buffer) throws QpackException { - long streamId = _integerParser.decodeInt(buffer); + long streamId = _integerDecoder.decodeInt(buffer); if (streamId >= 0) { reset(); @@ -113,7 +113,7 @@ public class EncoderInstructionParser private void parseStreamCancellation(ByteBuffer buffer) throws QpackException { - long streamId = _integerParser.decodeLong(buffer); + long streamId = _integerDecoder.decodeLong(buffer); if (streamId >= 0) { reset(); @@ -123,7 +123,7 @@ public class EncoderInstructionParser private void parseInsertCountIncrement(ByteBuffer buffer) throws QpackException { - int increment = _integerParser.decodeInt(buffer); + int increment = _integerDecoder.decodeInt(buffer); if (increment >= 0) { reset(); @@ -134,6 +134,6 @@ public class EncoderInstructionParser public void reset() { _state = State.IDLE; - _integerParser.reset(); + _integerDecoder.reset(); } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/table/Entry.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/table/Entry.java index 7af81578208..9f7f7af4c06 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/table/Entry.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/table/Entry.java @@ -17,8 +17,8 @@ import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanEncoder; -import org.eclipse.jetty.http3.qpack.internal.util.NBitIntegerEncoder; +import org.eclipse.jetty.http.compression.HuffmanEncoder; +import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.util.StringUtil; public class Entry @@ -119,7 +119,7 @@ public class Entry int huffmanLen = HuffmanEncoder.octetsNeeded(value); if (huffmanLen < 0) throw new IllegalStateException("bad value"); - int lenLen = NBitIntegerEncoder.octectsNeeded(7, huffmanLen); + int lenLen = NBitIntegerEncoder.octetsNeeded(7, huffmanLen); _huffmanValue = new byte[1 + lenLen + huffmanLen]; ByteBuffer buffer = ByteBuffer.wrap(_huffmanValue); diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanEncoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanEncoder.java deleted file mode 100644 index 2ce68d01706..00000000000 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/util/HuffmanEncoder.java +++ /dev/null @@ -1,473 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.http3.qpack.internal.util; - -import java.nio.ByteBuffer; - -public class HuffmanEncoder -{ - - // Appendix C: Huffman Codes - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C - static final int[][] CODES = - { - /* ( 0) |11111111|11000 */ {0x1ff8, 13}, - /* ( 1) |11111111|11111111|1011000 */ {0x7fffd8, 23}, - /* ( 2) |11111111|11111111|11111110|0010 */ {0xfffffe2, 28}, - /* ( 3) |11111111|11111111|11111110|0011 */ {0xfffffe3, 28}, - /* ( 4) |11111111|11111111|11111110|0100 */ {0xfffffe4, 28}, - /* ( 5) |11111111|11111111|11111110|0101 */ {0xfffffe5, 28}, - /* ( 6) |11111111|11111111|11111110|0110 */ {0xfffffe6, 28}, - /* ( 7) |11111111|11111111|11111110|0111 */ {0xfffffe7, 28}, - /* ( 8) |11111111|11111111|11111110|1000 */ {0xfffffe8, 28}, - /* ( 9) |11111111|11111111|11101010 */ {0xffffea, 24}, - /* ( 10) |11111111|11111111|11111111|111100 */ {0x3ffffffc, 30}, - /* ( 11) |11111111|11111111|11111110|1001 */ {0xfffffe9, 28}, - /* ( 12) |11111111|11111111|11111110|1010 */ {0xfffffea, 28}, - /* ( 13) |11111111|11111111|11111111|111101 */ {0x3ffffffd, 30}, - /* ( 14) |11111111|11111111|11111110|1011 */ {0xfffffeb, 28}, - /* ( 15) |11111111|11111111|11111110|1100 */ {0xfffffec, 28}, - /* ( 16) |11111111|11111111|11111110|1101 */ {0xfffffed, 28}, - /* ( 17) |11111111|11111111|11111110|1110 */ {0xfffffee, 28}, - /* ( 18) |11111111|11111111|11111110|1111 */ {0xfffffef, 28}, - /* ( 19) |11111111|11111111|11111111|0000 */ {0xffffff0, 28}, - /* ( 20) |11111111|11111111|11111111|0001 */ {0xffffff1, 28}, - /* ( 21) |11111111|11111111|11111111|0010 */ {0xffffff2, 28}, - /* ( 22) |11111111|11111111|11111111|111110 */ {0x3ffffffe, 30}, - /* ( 23) |11111111|11111111|11111111|0011 */ {0xffffff3, 28}, - /* ( 24) |11111111|11111111|11111111|0100 */ {0xffffff4, 28}, - /* ( 25) |11111111|11111111|11111111|0101 */ {0xffffff5, 28}, - /* ( 26) |11111111|11111111|11111111|0110 */ {0xffffff6, 28}, - /* ( 27) |11111111|11111111|11111111|0111 */ {0xffffff7, 28}, - /* ( 28) |11111111|11111111|11111111|1000 */ {0xffffff8, 28}, - /* ( 29) |11111111|11111111|11111111|1001 */ {0xffffff9, 28}, - /* ( 30) |11111111|11111111|11111111|1010 */ {0xffffffa, 28}, - /* ( 31) |11111111|11111111|11111111|1011 */ {0xffffffb, 28}, - /*' ' ( 32) |010100 */ {0x14, 6}, - /*'!' ( 33) |11111110|00 */ {0x3f8, 10}, - /*'"' ( 34) |11111110|01 */ {0x3f9, 10}, - /*'#' ( 35) |11111111|1010 */ {0xffa, 12}, - /*'$' ( 36) |11111111|11001 */ {0x1ff9, 13}, - /*'%' ( 37) |010101 */ {0x15, 6}, - /*'&' ( 38) |11111000 */ {0xf8, 8}, - /*''' ( 39) |11111111|010 */ {0x7fa, 11}, - /*'(' ( 40) |11111110|10 */ {0x3fa, 10}, - /*')' ( 41) |11111110|11 */ {0x3fb, 10}, - /*'*' ( 42) |11111001 */ {0xf9, 8}, - /*'+' ( 43) |11111111|011 */ {0x7fb, 11}, - /*',' ( 44) |11111010 */ {0xfa, 8}, - /*'-' ( 45) |010110 */ {0x16, 6}, - /*'.' ( 46) |010111 */ {0x17, 6}, - /*'/' ( 47) |011000 */ {0x18, 6}, - /*'0' ( 48) |00000 */ {0x0, 5}, - /*'1' ( 49) |00001 */ {0x1, 5}, - /*'2' ( 50) |00010 */ {0x2, 5}, - /*'3' ( 51) |011001 */ {0x19, 6}, - /*'4' ( 52) |011010 */ {0x1a, 6}, - /*'5' ( 53) |011011 */ {0x1b, 6}, - /*'6' ( 54) |011100 */ {0x1c, 6}, - /*'7' ( 55) |011101 */ {0x1d, 6}, - /*'8' ( 56) |011110 */ {0x1e, 6}, - /*'9' ( 57) |011111 */ {0x1f, 6}, - /*':' ( 58) |1011100 */ {0x5c, 7}, - /*';' ( 59) |11111011 */ {0xfb, 8}, - /*'<' ( 60) |11111111|1111100 */ {0x7ffc, 15}, - /*'=' ( 61) |100000 */ {0x20, 6}, - /*'>' ( 62) |11111111|1011 */ {0xffb, 12}, - /*'?' ( 63) |11111111|00 */ {0x3fc, 10}, - /*'@' ( 64) |11111111|11010 */ {0x1ffa, 13}, - /*'A' ( 65) |100001 */ {0x21, 6}, - /*'B' ( 66) |1011101 */ {0x5d, 7}, - /*'C' ( 67) |1011110 */ {0x5e, 7}, - /*'D' ( 68) |1011111 */ {0x5f, 7}, - /*'E' ( 69) |1100000 */ {0x60, 7}, - /*'F' ( 70) |1100001 */ {0x61, 7}, - /*'G' ( 71) |1100010 */ {0x62, 7}, - /*'H' ( 72) |1100011 */ {0x63, 7}, - /*'I' ( 73) |1100100 */ {0x64, 7}, - /*'J' ( 74) |1100101 */ {0x65, 7}, - /*'K' ( 75) |1100110 */ {0x66, 7}, - /*'L' ( 76) |1100111 */ {0x67, 7}, - /*'M' ( 77) |1101000 */ {0x68, 7}, - /*'N' ( 78) |1101001 */ {0x69, 7}, - /*'O' ( 79) |1101010 */ {0x6a, 7}, - /*'P' ( 80) |1101011 */ {0x6b, 7}, - /*'Q' ( 81) |1101100 */ {0x6c, 7}, - /*'R' ( 82) |1101101 */ {0x6d, 7}, - /*'S' ( 83) |1101110 */ {0x6e, 7}, - /*'T' ( 84) |1101111 */ {0x6f, 7}, - /*'U' ( 85) |1110000 */ {0x70, 7}, - /*'V' ( 86) |1110001 */ {0x71, 7}, - /*'W' ( 87) |1110010 */ {0x72, 7}, - /*'X' ( 88) |11111100 */ {0xfc, 8}, - /*'Y' ( 89) |1110011 */ {0x73, 7}, - /*'Z' ( 90) |11111101 */ {0xfd, 8}, - /*'[' ( 91) |11111111|11011 */ {0x1ffb, 13}, - /*'\' ( 92) |11111111|11111110|000 */ {0x7fff0, 19}, - /*']' ( 93) |11111111|11100 */ {0x1ffc, 13}, - /*'^' ( 94) |11111111|111100 */ {0x3ffc, 14}, - /*'_' ( 95) |100010 */ {0x22, 6}, - /*'`' ( 96) |11111111|1111101 */ {0x7ffd, 15}, - /*'a' ( 97) |00011 */ {0x3, 5}, - /*'b' ( 98) |100011 */ {0x23, 6}, - /*'c' ( 99) |00100 */ {0x4, 5}, - /*'d' (100) |100100 */ {0x24, 6}, - /*'e' (101) |00101 */ {0x5, 5}, - /*'f' (102) |100101 */ {0x25, 6}, - /*'g' (103) |100110 */ {0x26, 6}, - /*'h' (104) |100111 */ {0x27, 6}, - /*'i' (105) |00110 */ {0x6, 5}, - /*'j' (106) |1110100 */ {0x74, 7}, - /*'k' (107) |1110101 */ {0x75, 7}, - /*'l' (108) |101000 */ {0x28, 6}, - /*'m' (109) |101001 */ {0x29, 6}, - /*'n' (110) |101010 */ {0x2a, 6}, - /*'o' (111) |00111 */ {0x7, 5}, - /*'p' (112) |101011 */ {0x2b, 6}, - /*'q' (113) |1110110 */ {0x76, 7}, - /*'r' (114) |101100 */ {0x2c, 6}, - /*'s' (115) |01000 */ {0x8, 5}, - /*'t' (116) |01001 */ {0x9, 5}, - /*'u' (117) |101101 */ {0x2d, 6}, - /*'v' (118) |1110111 */ {0x77, 7}, - /*'w' (119) |1111000 */ {0x78, 7}, - /*'x' (120) |1111001 */ {0x79, 7}, - /*'y' (121) |1111010 */ {0x7a, 7}, - /*'z' (122) |1111011 */ {0x7b, 7}, - /*'{' (123) |11111111|1111110 */ {0x7ffe, 15}, - /*'|' (124) |11111111|100 */ {0x7fc, 11}, - /*'}' (125) |11111111|111101 */ {0x3ffd, 14}, - /*'~' (126) |11111111|11101 */ {0x1ffd, 13}, - /* (127) |11111111|11111111|11111111|1100 */ {0xffffffc, 28}, - /* (128) |11111111|11111110|0110 */ {0xfffe6, 20}, - /* (129) |11111111|11111111|010010 */ {0x3fffd2, 22}, - /* (130) |11111111|11111110|0111 */ {0xfffe7, 20}, - /* (131) |11111111|11111110|1000 */ {0xfffe8, 20}, - /* (132) |11111111|11111111|010011 */ {0x3fffd3, 22}, - /* (133) |11111111|11111111|010100 */ {0x3fffd4, 22}, - /* (134) |11111111|11111111|010101 */ {0x3fffd5, 22}, - /* (135) |11111111|11111111|1011001 */ {0x7fffd9, 23}, - /* (136) |11111111|11111111|010110 */ {0x3fffd6, 22}, - /* (137) |11111111|11111111|1011010 */ {0x7fffda, 23}, - /* (138) |11111111|11111111|1011011 */ {0x7fffdb, 23}, - /* (139) |11111111|11111111|1011100 */ {0x7fffdc, 23}, - /* (140) |11111111|11111111|1011101 */ {0x7fffdd, 23}, - /* (141) |11111111|11111111|1011110 */ {0x7fffde, 23}, - /* (142) |11111111|11111111|11101011 */ {0xffffeb, 24}, - /* (143) |11111111|11111111|1011111 */ {0x7fffdf, 23}, - /* (144) |11111111|11111111|11101100 */ {0xffffec, 24}, - /* (145) |11111111|11111111|11101101 */ {0xffffed, 24}, - /* (146) |11111111|11111111|010111 */ {0x3fffd7, 22}, - /* (147) |11111111|11111111|1100000 */ {0x7fffe0, 23}, - /* (148) |11111111|11111111|11101110 */ {0xffffee, 24}, - /* (149) |11111111|11111111|1100001 */ {0x7fffe1, 23}, - /* (150) |11111111|11111111|1100010 */ {0x7fffe2, 23}, - /* (151) |11111111|11111111|1100011 */ {0x7fffe3, 23}, - /* (152) |11111111|11111111|1100100 */ {0x7fffe4, 23}, - /* (153) |11111111|11111110|11100 */ {0x1fffdc, 21}, - /* (154) |11111111|11111111|011000 */ {0x3fffd8, 22}, - /* (155) |11111111|11111111|1100101 */ {0x7fffe5, 23}, - /* (156) |11111111|11111111|011001 */ {0x3fffd9, 22}, - /* (157) |11111111|11111111|1100110 */ {0x7fffe6, 23}, - /* (158) |11111111|11111111|1100111 */ {0x7fffe7, 23}, - /* (159) |11111111|11111111|11101111 */ {0xffffef, 24}, - /* (160) |11111111|11111111|011010 */ {0x3fffda, 22}, - /* (161) |11111111|11111110|11101 */ {0x1fffdd, 21}, - /* (162) |11111111|11111110|1001 */ {0xfffe9, 20}, - /* (163) |11111111|11111111|011011 */ {0x3fffdb, 22}, - /* (164) |11111111|11111111|011100 */ {0x3fffdc, 22}, - /* (165) |11111111|11111111|1101000 */ {0x7fffe8, 23}, - /* (166) |11111111|11111111|1101001 */ {0x7fffe9, 23}, - /* (167) |11111111|11111110|11110 */ {0x1fffde, 21}, - /* (168) |11111111|11111111|1101010 */ {0x7fffea, 23}, - /* (169) |11111111|11111111|011101 */ {0x3fffdd, 22}, - /* (170) |11111111|11111111|011110 */ {0x3fffde, 22}, - /* (171) |11111111|11111111|11110000 */ {0xfffff0, 24}, - /* (172) |11111111|11111110|11111 */ {0x1fffdf, 21}, - /* (173) |11111111|11111111|011111 */ {0x3fffdf, 22}, - /* (174) |11111111|11111111|1101011 */ {0x7fffeb, 23}, - /* (175) |11111111|11111111|1101100 */ {0x7fffec, 23}, - /* (176) |11111111|11111111|00000 */ {0x1fffe0, 21}, - /* (177) |11111111|11111111|00001 */ {0x1fffe1, 21}, - /* (178) |11111111|11111111|100000 */ {0x3fffe0, 22}, - /* (179) |11111111|11111111|00010 */ {0x1fffe2, 21}, - /* (180) |11111111|11111111|1101101 */ {0x7fffed, 23}, - /* (181) |11111111|11111111|100001 */ {0x3fffe1, 22}, - /* (182) |11111111|11111111|1101110 */ {0x7fffee, 23}, - /* (183) |11111111|11111111|1101111 */ {0x7fffef, 23}, - /* (184) |11111111|11111110|1010 */ {0xfffea, 20}, - /* (185) |11111111|11111111|100010 */ {0x3fffe2, 22}, - /* (186) |11111111|11111111|100011 */ {0x3fffe3, 22}, - /* (187) |11111111|11111111|100100 */ {0x3fffe4, 22}, - /* (188) |11111111|11111111|1110000 */ {0x7ffff0, 23}, - /* (189) |11111111|11111111|100101 */ {0x3fffe5, 22}, - /* (190) |11111111|11111111|100110 */ {0x3fffe6, 22}, - /* (191) |11111111|11111111|1110001 */ {0x7ffff1, 23}, - /* (192) |11111111|11111111|11111000|00 */ {0x3ffffe0, 26}, - /* (193) |11111111|11111111|11111000|01 */ {0x3ffffe1, 26}, - /* (194) |11111111|11111110|1011 */ {0xfffeb, 20}, - /* (195) |11111111|11111110|001 */ {0x7fff1, 19}, - /* (196) |11111111|11111111|100111 */ {0x3fffe7, 22}, - /* (197) |11111111|11111111|1110010 */ {0x7ffff2, 23}, - /* (198) |11111111|11111111|101000 */ {0x3fffe8, 22}, - /* (199) |11111111|11111111|11110110|0 */ {0x1ffffec, 25}, - /* (200) |11111111|11111111|11111000|10 */ {0x3ffffe2, 26}, - /* (201) |11111111|11111111|11111000|11 */ {0x3ffffe3, 26}, - /* (202) |11111111|11111111|11111001|00 */ {0x3ffffe4, 26}, - /* (203) |11111111|11111111|11111011|110 */ {0x7ffffde, 27}, - /* (204) |11111111|11111111|11111011|111 */ {0x7ffffdf, 27}, - /* (205) |11111111|11111111|11111001|01 */ {0x3ffffe5, 26}, - /* (206) |11111111|11111111|11110001 */ {0xfffff1, 24}, - /* (207) |11111111|11111111|11110110|1 */ {0x1ffffed, 25}, - /* (208) |11111111|11111110|010 */ {0x7fff2, 19}, - /* (209) |11111111|11111111|00011 */ {0x1fffe3, 21}, - /* (210) |11111111|11111111|11111001|10 */ {0x3ffffe6, 26}, - /* (211) |11111111|11111111|11111100|000 */ {0x7ffffe0, 27}, - /* (212) |11111111|11111111|11111100|001 */ {0x7ffffe1, 27}, - /* (213) |11111111|11111111|11111001|11 */ {0x3ffffe7, 26}, - /* (214) |11111111|11111111|11111100|010 */ {0x7ffffe2, 27}, - /* (215) |11111111|11111111|11110010 */ {0xfffff2, 24}, - /* (216) |11111111|11111111|00100 */ {0x1fffe4, 21}, - /* (217) |11111111|11111111|00101 */ {0x1fffe5, 21}, - /* (218) |11111111|11111111|11111010|00 */ {0x3ffffe8, 26}, - /* (219) |11111111|11111111|11111010|01 */ {0x3ffffe9, 26}, - /* (220) |11111111|11111111|11111111|1101 */ {0xffffffd, 28}, - /* (221) |11111111|11111111|11111100|011 */ {0x7ffffe3, 27}, - /* (222) |11111111|11111111|11111100|100 */ {0x7ffffe4, 27}, - /* (223) |11111111|11111111|11111100|101 */ {0x7ffffe5, 27}, - /* (224) |11111111|11111110|1100 */ {0xfffec, 20}, - /* (225) |11111111|11111111|11110011 */ {0xfffff3, 24}, - /* (226) |11111111|11111110|1101 */ {0xfffed, 20}, - /* (227) |11111111|11111111|00110 */ {0x1fffe6, 21}, - /* (228) |11111111|11111111|101001 */ {0x3fffe9, 22}, - /* (229) |11111111|11111111|00111 */ {0x1fffe7, 21}, - /* (230) |11111111|11111111|01000 */ {0x1fffe8, 21}, - /* (231) |11111111|11111111|1110011 */ {0x7ffff3, 23}, - /* (232) |11111111|11111111|101010 */ {0x3fffea, 22}, - /* (233) |11111111|11111111|101011 */ {0x3fffeb, 22}, - /* (234) |11111111|11111111|11110111|0 */ {0x1ffffee, 25}, - /* (235) |11111111|11111111|11110111|1 */ {0x1ffffef, 25}, - /* (236) |11111111|11111111|11110100 */ {0xfffff4, 24}, - /* (237) |11111111|11111111|11110101 */ {0xfffff5, 24}, - /* (238) |11111111|11111111|11111010|10 */ {0x3ffffea, 26}, - /* (239) |11111111|11111111|1110100 */ {0x7ffff4, 23}, - /* (240) |11111111|11111111|11111010|11 */ {0x3ffffeb, 26}, - /* (241) |11111111|11111111|11111100|110 */ {0x7ffffe6, 27}, - /* (242) |11111111|11111111|11111011|00 */ {0x3ffffec, 26}, - /* (243) |11111111|11111111|11111011|01 */ {0x3ffffed, 26}, - /* (244) |11111111|11111111|11111100|111 */ {0x7ffffe7, 27}, - /* (245) |11111111|11111111|11111101|000 */ {0x7ffffe8, 27}, - /* (246) |11111111|11111111|11111101|001 */ {0x7ffffe9, 27}, - /* (247) |11111111|11111111|11111101|010 */ {0x7ffffea, 27}, - /* (248) |11111111|11111111|11111101|011 */ {0x7ffffeb, 27}, - /* (249) |11111111|11111111|11111111|1110 */ {0xffffffe, 28}, - /* (250) |11111111|11111111|11111101|100 */ {0x7ffffec, 27}, - /* (251) |11111111|11111111|11111101|101 */ {0x7ffffed, 27}, - /* (252) |11111111|11111111|11111101|110 */ {0x7ffffee, 27}, - /* (253) |11111111|11111111|11111101|111 */ {0x7ffffef, 27}, - /* (254) |11111111|11111111|11111110|000 */ {0x7fffff0, 27}, - /* (255) |11111111|11111111|11111011|10 */ {0x3ffffee, 26}, - /*EOS (256) |11111111|11111111|11111111|111111 */ {0x3fffffff, 30} - }; - - static final int[][] LCCODES = new int[CODES.length][]; - static final char EOS = 256; - - // Huffman decode tree stored in a flattened char array for good - // locality of reference. - static final char[] tree; - static final char[] rowsym; - static final byte[] rowbits; - - // Build the Huffman lookup tree and LC TABLE - static - { - System.arraycopy(CODES, 0, LCCODES, 0, CODES.length); - for (int i = 'A'; i <= 'Z'; i++) - { - LCCODES[i] = LCCODES['a' + i - 'A']; - } - - int r = 0; - for (int i = 0; i < CODES.length; i++) - { - r += (CODES[i][1] + 7) / 8; - } - tree = new char[r * 256]; - rowsym = new char[r]; - rowbits = new byte[r]; - - r = 0; - for (int sym = 0; sym < CODES.length; sym++) - { - int code = CODES[sym][0]; - int len = CODES[sym][1]; - - int current = 0; - - while (len > 8) - { - len -= 8; - int i = ((code >>> len) & 0xFF); - - int t = current * 256 + i; - current = tree[t]; - if (current == 0) - { - tree[t] = (char)++r; - current = r; - } - } - - int terminal = ++r; - rowsym[r] = (char)sym; - int b = len & 0x07; - int terminalBits = b == 0 ? 8 : b; - - rowbits[r] = (byte)terminalBits; - int shift = 8 - len; - int start = current * 256 + ((code << shift) & 0xFF); - int end = start + (1 << shift); - for (int i = start; i < end; i++) - { - tree[i] = (char)terminal; - } - } - } - - public static int octetsNeeded(String s) - { - return octetsNeeded(CODES, s); - } - - public static int octetsNeeded(byte[] b) - { - return octetsNeeded(CODES, b); - } - - public static void encode(ByteBuffer buffer, String s) - { - encode(CODES, buffer, s); - } - - public static void encode(ByteBuffer buffer, byte[] b) - { - encode(CODES, buffer, b); - } - - public static int octetsNeededLC(String s) - { - return octetsNeeded(LCCODES, s); - } - - public static void encodeLC(ByteBuffer buffer, String s) - { - encode(LCCODES, buffer, s); - } - - private static int octetsNeeded(final int[][] table, String s) - { - int needed = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - return -1; - needed += table[c][1]; - } - - return (needed + 7) / 8; - } - - private static int octetsNeeded(final int[][] table, byte[] b) - { - int needed = 0; - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - needed += table[c][1]; - } - return (needed + 7) / 8; - } - - /** - * @param table The table to encode by - * @param buffer The buffer to encode to - * @param s The string to encode - */ - private static void encode(final int[][] table, ByteBuffer buffer, String s) - { - long current = 0; - int n = 0; - int len = s.length(); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c >= 128 || c < ' ') - throw new IllegalArgumentException(); - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } - - private static void encode(final int[][] table, ByteBuffer buffer, byte[] b) - { - long current = 0; - int n = 0; - - int len = b.length; - for (int i = 0; i < len; i++) - { - int c = 0xFF & b[i]; - int code = table[c][0]; - int bits = table[c][1]; - - current <<= bits; - current |= code; - n += bits; - - while (n >= 8) - { - n -= 8; - buffer.put((byte)(current >> n)); - } - } - - if (n > 0) - { - current <<= (8 - n); - current |= (0xFF >>> n); - buffer.put((byte)(current)); - } - } -} diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParserTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParserTest.java index 6bcc7ab239e..460a4e92c0e 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParserTest.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParserTest.java @@ -14,18 +14,26 @@ package org.eclipse.jetty.http3.qpack; import java.nio.ByteBuffer; +import java.util.List; +import org.eclipse.jetty.http3.qpack.internal.instruction.DuplicateInstruction; +import org.eclipse.jetty.http3.qpack.internal.instruction.IndexedNameEntryInstruction; +import org.eclipse.jetty.http3.qpack.internal.instruction.SetCapacityInstruction; import org.eclipse.jetty.http3.qpack.internal.parser.DecoderInstructionParser; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.NullByteBufferPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class DecoderInstructionParserTest { + private final NullByteBufferPool bufferPool = new NullByteBufferPool(); private DecoderInstructionParser _instructionParser; private DecoderParserDebugHandler _handler; @@ -41,6 +49,11 @@ public class DecoderInstructionParserTest { // Set Dynamic Table Capacity=220. ByteBuffer buffer = QpackTestUtil.hexToBuffer("3fbd 01"); + + // Assert that our generated value is equal to that of the spec example. + ByteBuffer encodedValue = getEncodedValue(new SetCapacityInstruction(220)); + assertThat(buffer, equalTo(encodedValue)); + _instructionParser.parse(buffer); assertThat(_handler.setCapacities.poll(), is(220)); assertTrue(_handler.isEmpty()); @@ -51,6 +64,11 @@ public class DecoderInstructionParserTest { // Duplicate (Relative Index = 2). ByteBuffer buffer = QpackTestUtil.hexToBuffer("02"); + + // Assert that our generated value is equal to that of the spec example. + ByteBuffer encodedValue = getEncodedValue(new DuplicateInstruction(2)); + assertThat(buffer, equalTo(encodedValue)); + _instructionParser.parse(buffer); assertThat(_handler.duplicates.poll(), is(2)); assertTrue(_handler.isEmpty()); @@ -61,6 +79,11 @@ public class DecoderInstructionParserTest { // Insert With Name Reference to Static Table, Index=0 (:authority=www.example.com). ByteBuffer buffer = QpackTestUtil.hexToBuffer("c00f 7777 772e 6578 616d 706c 652e 636f 6d"); + + // Assert that our generated value is equal to that of the spec example. + ByteBuffer encodedValue = getEncodedValue(new IndexedNameEntryInstruction(false, 0, false, "www.example.com")); + assertThat(buffer, equalTo(encodedValue)); + _instructionParser.parse(buffer); DecoderParserDebugHandler.ReferencedEntry entry = _handler.referencedNameEntries.poll(); assertNotNull(entry); @@ -94,4 +117,13 @@ public class DecoderInstructionParserTest // There are no other instructions received. assertTrue(_handler.isEmpty()); } + + private ByteBuffer getEncodedValue(Instruction instruction) + { + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(bufferPool); + instruction.encode(lease); + List byteBuffers = lease.getByteBuffers(); + assertThat(byteBuffers.size(), equalTo(1)); + return byteBuffers.get(0); + } } diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/HuffmanTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/HuffmanTest.java deleted file mode 100644 index 189803ce310..00000000000 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/HuffmanTest.java +++ /dev/null @@ -1,86 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.http3.qpack; - -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.util.Locale; -import java.util.stream.Stream; - -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanDecoder; -import org.eclipse.jetty.http3.qpack.internal.util.HuffmanEncoder; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.TypeUtil; -import org.hamcrest.Matchers; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class HuffmanTest -{ - public static Stream data() - { - return Stream.of( - new String[][]{ - {"D.4.1", "f1e3c2e5f23a6ba0ab90f4ff", "www.example.com"}, - {"D.4.2", "a8eb10649cbf", "no-cache"}, - {"D.6.1k", "6402", "302"}, - {"D.6.1v", "aec3771a4b", "private"}, - {"D.6.1d", "d07abe941054d444a8200595040b8166e082a62d1bff", "Mon, 21 Oct 2013 20:13:21 GMT"}, - {"D.6.1l", "9d29ad171863c78f0b97c8e9ae82ae43d3", "https://www.example.com"}, - {"D.6.2te", "640cff", "303"}, - }).map(Arguments::of); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testDecode(String specSection, String hex, String expected) throws Exception - { - byte[] encoded = TypeUtil.fromHexString(hex); - HuffmanDecoder huffmanDecoder = new HuffmanDecoder(); - huffmanDecoder.setLength(encoded.length); - String decoded = huffmanDecoder.decode(ByteBuffer.wrap(encoded)); - assertEquals(expected, decoded, specSection); - } - - @ParameterizedTest(name = "[{index}] spec={0}") - @MethodSource("data") - public void testEncode(String specSection, String hex, String expected) - { - ByteBuffer buf = BufferUtil.allocate(1024); - int pos = BufferUtil.flipToFill(buf); - HuffmanEncoder.encode(buf, expected); - BufferUtil.flipToFlush(buf, pos); - String encoded = TypeUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH); - assertEquals(hex, encoded, specSection); - assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected)); - } - - @ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name - @ValueSource(chars = {(char)128, (char)0, (char)-1, ' ' - 1}) - public void testEncode8859Only(char bad) - { - String s = "bad '" + bad + "'"; - - assertThat(HuffmanEncoder.octetsNeeded(s), Matchers.is(-1)); - - assertThrows(BufferOverflowException.class, - () -> HuffmanEncoder.encode(BufferUtil.allocate(32), s)); - } -} diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/QpackTestUtil.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/QpackTestUtil.java index 5125867646b..04f699e8e1a 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/QpackTestUtil.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/QpackTestUtil.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.http3.qpack; import java.nio.ByteBuffer; import java.util.List; +import java.util.Objects; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; @@ -127,4 +128,15 @@ public class QpackTestUtil { return new MetaData(HttpVersion.HTTP_3, fields); } + + public static boolean compareMetaData(MetaData m1, MetaData m2) + { + if (!Objects.equals(m1.getHttpVersion(), m2.getHttpVersion())) + return false; + if (!Objects.equals(m1.getContentLength(), m2.getContentLength())) + return false; + if (!Objects.equals(m1.getFields(), m2.getFields())) + return false; + return m1.getTrailerSupplier() == null && m2.getTrailerSupplier() == null; + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java new file mode 100644 index 00000000000..341f39fda07 --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java @@ -0,0 +1,277 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +/** + *

Build a string from a sequence of bytes.

+ *

Implementations of this interface are optimized for processing a mix of calls to already decoded + * character based appends (e.g. {@link #append(char)} and calls to undecoded byte methods (e.g. {@link #append(byte)}. + * This is particularly useful for decoding % encoded strings that are mostly already decoded but may contain + * escaped byte sequences that are not decoded. The standard {@link CharsetDecoder} API is not well suited for this + * use-case.

+ *

Any coding errors in the string will be reported by a {@link CharacterCodingException} thrown + * from the {@link #build()} method.

+ * @see Utf8StringBuilder for UTF-8 decoding with replacement of coding errors and/or fast fail behaviour. + * @see CharsetDecoder for decoding arbitrary {@link Charset}s with control over {@link CodingErrorAction}. + */ +public interface CharsetStringBuilder +{ + void append(byte b); + + void append(char c); + + default void append(byte[] bytes) + { + append(bytes, 0, bytes.length); + } + + default void append(byte[] b, int offset, int length) + { + int end = offset + length; + for (int i = offset; i < end; i++) + append(b[i]); + } + + default void append(CharSequence chars, int offset, int length) + { + int end = offset + length; + for (int i = offset; i < end; i++) + append(chars.charAt(i)); + } + + default void append(ByteBuffer buf) + { + int end = buf.position() + buf.remaining(); + while (buf.position() < end) + append(buf.get()); + } + + /** + *

Build the completed string and reset the buffer.

+ * @return The decoded built string which must be complete in regard to any multibyte sequences. + * @throws CharacterCodingException If the bytes cannot be correctly decoded or a multibyte sequence is incomplete. + */ + String build() throws CharacterCodingException; + + void reset(); + + static CharsetStringBuilder forCharset(Charset charset) + { + Objects.requireNonNull(charset); + if (charset == StandardCharsets.ISO_8859_1) + return new Iso88591StringBuilder(); + if (charset == StandardCharsets.US_ASCII) + return new UsAsciiStringBuilder(); + + // Use a CharsetDecoder that defaults to CodingErrorAction#REPORT + return new DecoderStringBuilder(charset.newDecoder()); + } + + class Iso88591StringBuilder implements CharsetStringBuilder + { + private final StringBuilder _builder = new StringBuilder(); + + @Override + public void append(byte b) + { + _builder.append((char)(0xff & b)); + } + + @Override + public void append(char c) + { + _builder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + _builder.append(chars, offset, length); + } + + @Override + public String build() + { + String s = _builder.toString(); + _builder.setLength(0); + return s; + } + + @Override + public void reset() + { + _builder.setLength(0); + } + } + + class UsAsciiStringBuilder implements CharsetStringBuilder + { + private final StringBuilder _builder = new StringBuilder(); + + @Override + public void append(byte b) + { + if (b < 0) + throw new IllegalArgumentException(); + _builder.append((char)b); + } + + @Override + public void append(char c) + { + _builder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + _builder.append(chars, offset, length); + } + + @Override + public String build() + { + String s = _builder.toString(); + _builder.setLength(0); + return s; + } + + @Override + public void reset() + { + _builder.setLength(0); + } + } + + class DecoderStringBuilder implements CharsetStringBuilder + { + private final CharsetDecoder _decoder; + private final StringBuilder _stringBuilder = new StringBuilder(32); + private ByteBuffer _buffer = ByteBuffer.allocate(32); + + public DecoderStringBuilder(CharsetDecoder charsetDecoder) + { + _decoder = charsetDecoder; + } + + private void ensureSpace(int needed) + { + int space = _buffer.remaining(); + if (space < needed) + { + int position = _buffer.position(); + _buffer = ByteBuffer.wrap(Arrays.copyOf(_buffer.array(), _buffer.capacity() + needed - space + 32)).position(position); + } + } + + @Override + public void append(byte b) + { + ensureSpace(1); + _buffer.put(b); + } + + @Override + public void append(char c) + { + if (_buffer.position() > 0) + { + try + { + // Append any data already in the decoder + _stringBuilder.append(_decoder.decode(_buffer.flip())); + _buffer.clear(); + } + catch (CharacterCodingException e) + { + // This will be thrown only if the decoder is configured to REPORT, + // otherwise errors will be ignored or replaced and we will not catch here. + throw new RuntimeException(e); + } + } + _stringBuilder.append(c); + } + + @Override + public void append(CharSequence chars, int offset, int length) + { + if (_buffer.position() > 0) + { + try + { + // Append any data already in the decoder + _stringBuilder.append(_decoder.decode(_buffer.flip())); + _buffer.clear(); + } + catch (CharacterCodingException e) + { + // This will be thrown only if the decoder is configured to REPORT, + // otherwise errors will be ignored or replaced and we will not catch here. + throw new RuntimeException(e); + } + } + _stringBuilder.append(chars, offset, offset + length); + } + + @Override + public void append(byte[] b, int offset, int length) + { + ensureSpace(length); + _buffer.put(b, offset, length); + } + + @Override + public void append(ByteBuffer buf) + { + ensureSpace(buf.remaining()); + _buffer.put(buf); + } + + @Override + public String build() throws CharacterCodingException + { + try + { + if (_buffer.position() > 0) + { + CharSequence decoded = _decoder.decode(_buffer.flip()); + _buffer.clear(); + if (_stringBuilder.length() == 0) + return decoded.toString(); + _stringBuilder.append(decoded); + } + return _stringBuilder.toString(); + } + finally + { + _stringBuilder.setLength(0); + } + } + + @Override + public void reset() + { + _stringBuilder.setLength(0); + } + } +}