Issue #9554 - add javadoc for huffman / n-bit integer classes and remove static decode methods

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2023-04-18 15:24:59 +10:00
parent 09e6e6b211
commit a7b0b727dd
9 changed files with 174 additions and 32 deletions

View File

@ -13,6 +13,9 @@
package org.eclipse.jetty.http.compression; package org.eclipse.jetty.http.compression;
/**
* This class contains the Huffman Codes defined in RFC7541.
*/
public class Huffman public class Huffman
{ {
private Huffman() private Huffman()

View File

@ -21,20 +21,14 @@ import org.eclipse.jetty.util.CharsetStringBuilder;
import static org.eclipse.jetty.http.compression.Huffman.rowbits; import static org.eclipse.jetty.http.compression.Huffman.rowbits;
import static org.eclipse.jetty.http.compression.Huffman.rowsym; import static org.eclipse.jetty.http.compression.Huffman.rowsym;
/**
* <p>Used to decoded Huffman encoded strings.</p>
*
* <p>Characters which are illegal field-vchar values are replaced with
* either ' ' or '?' as described in RFC9110</p>
*/
public class HuffmanDecoder public class HuffmanDecoder
{ {
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;
}
private final CharsetStringBuilder.Iso8859StringBuilder _builder = new CharsetStringBuilder.Iso8859StringBuilder(); private final CharsetStringBuilder.Iso8859StringBuilder _builder = new CharsetStringBuilder.Iso8859StringBuilder();
private int _length = 0; private int _length = 0;
private int _count = 0; private int _count = 0;
@ -42,6 +36,9 @@ public class HuffmanDecoder
private int _current = 0; private int _current = 0;
private int _bits = 0; private int _bits = 0;
/**
* @param length in bytes of the huffman data.
*/
public void setLength(int length) public void setLength(int length)
{ {
if (_count != 0) if (_count != 0)
@ -49,6 +46,11 @@ public class HuffmanDecoder
_length = length; _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 public String decode(ByteBuffer buffer) throws EncodingException
{ {
for (; _count < _length; _count++) for (; _count < _length; _count++)

View File

@ -20,17 +20,31 @@ import org.eclipse.jetty.http.HttpTokens;
import static org.eclipse.jetty.http.compression.Huffman.CODES; import static org.eclipse.jetty.http.compression.Huffman.CODES;
import static org.eclipse.jetty.http.compression.Huffman.LCCODES; import static org.eclipse.jetty.http.compression.Huffman.LCCODES;
/**
* <p>Used to encode strings Huffman encoding.</p>
*
* <p>Characters are encoded with ISO-8859-1, if any multi-byte characters or
* control characters are present the encoder will throw {@link EncodingException}.</p>
*/
public class HuffmanEncoder public class HuffmanEncoder
{ {
private 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) public static int octetsNeeded(String s)
{ {
return octetsNeeded(CODES, 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) public static int octetsNeeded(byte[] b)
{ {
int needed = 0; int needed = 0;
@ -42,21 +56,37 @@ public class HuffmanEncoder
return (needed + 7) / 8; 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) public static void encode(ByteBuffer buffer, String s)
{ {
encode(CODES, buffer, s); encode(CODES, buffer, s);
} }
/**
* @param buffer the buffer to encode into.
* @param b the byte array to encode.
*/
public static void encode(ByteBuffer buffer, byte[] b) public static void encode(ByteBuffer buffer, byte[] b)
{ {
encode(CODES, buffer, b); encode(CODES, buffer, b);
} }
/**
* @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) public static int octetsNeededLowercase(String s)
{ {
return octetsNeeded(LCCODES, 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) public static void encodeLowercase(ByteBuffer buffer, String s)
{ {
encode(LCCODES, buffer, s); encode(LCCODES, buffer, s);

View File

@ -15,8 +15,20 @@ package org.eclipse.jetty.http.compression;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/**
* Used to encode integers as described in RFC7541.
*/
public class NBitIntegerEncoder public class NBitIntegerEncoder
{ {
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) public static int octetsNeeded(int n, long i)
{ {
if (n == 8) if (n == 8)
@ -43,6 +55,12 @@ public class NBitIntegerEncoder
return (log + 6) / 7; 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) public static void encode(ByteBuffer buf, int n, long i)
{ {
if (n == 8) if (n == 8)

View File

@ -15,28 +15,21 @@ package org.eclipse.jetty.http.compression;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/**
* Used to decode integers as described in RFC7541.
*/
public class NBitIntegerParser public class NBitIntegerParser
{ {
public static int decode(ByteBuffer buffer, int prefix) throws EncodingException
{
// TODO: 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);
NBitIntegerParser parser = new NBitIntegerParser();
parser.setPrefix(prefix);
int decodedInt = parser.decodeInt(buffer);
if (decodedInt < 0)
throw new EncodingException("invalid integer encoding");
parser.reset();
return decodedInt;
}
private int _prefix; private int _prefix;
private long _total; private long _total;
private long _multiplier; private long _multiplier;
private boolean _started; 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) public void setPrefix(int prefix)
{ {
if (_started) if (_started)
@ -44,11 +37,27 @@ public class NBitIntegerParser
_prefix = prefix; _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) public int decodeInt(ByteBuffer buffer)
{ {
return Math.toIntExact(decodeLong(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) public long decodeLong(ByteBuffer buffer)
{ {
if (!_started) if (!_started)
@ -86,6 +95,9 @@ public class NBitIntegerParser
} }
} }
/**
* Reset the internal state of the parser.
*/
public void reset() public void reset()
{ {
_prefix = 0; _prefix = 0;

View File

@ -17,6 +17,16 @@ import java.nio.ByteBuffer;
import org.eclipse.jetty.util.CharsetStringBuilder; import org.eclipse.jetty.util.CharsetStringBuilder;
/**
* <p>Used to decode string literals as described in RFC7541.</p>
*
* <p>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.</p>
*
* <p>Characters which are illegal field-vchar values are replaced with
* either ' ' or '?' as described in RFC9110</p>
*/
public class NBitStringParser public class NBitStringParser
{ {
private final NBitIntegerParser _integerParser; private final NBitIntegerParser _integerParser;
@ -43,6 +53,11 @@ public class NBitStringParser
_builder = new CharsetStringBuilder.Iso8859StringBuilder(); _builder = new CharsetStringBuilder.Iso8859StringBuilder();
} }
/**
* 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) public void setPrefix(int prefix)
{ {
if (_state != State.PARSING) if (_state != State.PARSING)
@ -50,6 +65,15 @@ public class NBitStringParser
_prefix = prefix; _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 public String decode(ByteBuffer buffer) throws EncodingException
{ {
while (true) while (true)

View File

@ -17,6 +17,7 @@ import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Stream; 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.HuffmanDecoder;
import org.eclipse.jetty.http.compression.HuffmanEncoder; import org.eclipse.jetty.http.compression.HuffmanEncoder;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
@ -34,6 +35,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class HuffmanTest 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<Arguments> data() public static Stream<Arguments> data()
{ {
return Stream.of( return Stream.of(
@ -94,7 +107,7 @@ public class HuffmanTest
public void testDecode8859Only(String hexString, char expected) throws Exception public void testDecode8859Only(String hexString, char expected) throws Exception
{ {
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(hexString)); ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(hexString));
String decoded = HuffmanDecoder.decode(buffer, buffer.remaining()); String decoded = decode(buffer, buffer.remaining());
assertThat(decoded, equalTo("" + expected)); assertThat(decoded, equalTo("" + expected));
} }
@ -146,6 +159,6 @@ public class HuffmanTest
private String decode(ByteBuffer buffer) throws Exception private String decode(ByteBuffer buffer) throws Exception
{ {
return HuffmanDecoder.decode(buffer, buffer.remaining()); return decode(buffer, buffer.remaining());
} }
} }

View File

@ -41,6 +41,7 @@ public class HpackDecoder
private final HpackContext _context; private final HpackContext _context;
private final MetaDataBuilder _builder; private final MetaDataBuilder _builder;
private final HuffmanDecoder _huffmanDecoder; private final HuffmanDecoder _huffmanDecoder;
private final NBitIntegerParser _integerParser;
private int _localMaxDynamicTableSize; private int _localMaxDynamicTableSize;
/** /**
@ -53,6 +54,7 @@ public class HpackDecoder
_localMaxDynamicTableSize = localMaxDynamicTableSize; _localMaxDynamicTableSize = localMaxDynamicTableSize;
_builder = new MetaDataBuilder(maxHeaderSize); _builder = new MetaDataBuilder(maxHeaderSize);
_huffmanDecoder = new HuffmanDecoder(); _huffmanDecoder = new HuffmanDecoder();
_integerParser = new NBitIntegerParser();
} }
public HpackContext getHpackContext() public HpackContext getHpackContext()
@ -277,7 +279,14 @@ public class HpackDecoder
{ {
try try
{ {
return NBitIntegerParser.decode(buffer, prefix); if (prefix != 8)
buffer.position(buffer.position() - 1);
_integerParser.setPrefix(prefix);
int decodedInt = _integerParser.decodeInt(buffer);
if (decodedInt < 0)
throw new EncodingException("invalid integer encoding");
return decodedInt;
} }
catch (EncodingException e) catch (EncodingException e)
{ {
@ -285,6 +294,10 @@ public class HpackDecoder
compressionException.initCause(e); compressionException.initCause(e);
throw compressionException; throw compressionException;
} }
finally
{
_integerParser.reset();
}
} }
private String huffmanDecode(ByteBuffer buffer, int length) throws HpackException.CompressionException private String huffmanDecode(ByteBuffer buffer, int length) throws HpackException.CompressionException

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpField; 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.HuffmanDecoder;
import org.eclipse.jetty.http.compression.NBitIntegerParser; import org.eclipse.jetty.http.compression.NBitIntegerParser;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
@ -34,6 +35,32 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/ */
public class HpackContextTest 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);
NBitIntegerParser parser = new NBitIntegerParser();
parser.setPrefix(prefix);
int decodedInt = parser.decodeInt(buffer);
if (decodedInt < 0)
throw new EncodingException("invalid integer encoding");
parser.reset();
return decodedInt;
}
@Test @Test
public void testStaticName() public void testStaticName()
@ -425,10 +452,10 @@ public class HpackContextTest
int huff = 0xff & buffer.get(); int huff = 0xff & buffer.get();
assertTrue((0x80 & huff) == 0x80); assertTrue((0x80 & huff) == 0x80);
int len = NBitIntegerParser.decode(buffer, 7); int len = decodeInt(buffer, 7);
assertEquals(len, buffer.remaining()); assertEquals(len, buffer.remaining());
String value = HuffmanDecoder.decode(buffer, buffer.remaining()); String value = decode(buffer, buffer.remaining());
assertEquals(entry.getHttpField().getValue(), value); assertEquals(entry.getHttpField().getValue(), value);
} }