Skip to content

Commit

Permalink
Issue #9554 - add javadoc for huffman / n-bit integer classes and rem…
Browse files Browse the repository at this point in the history
…ove static decode methods

Signed-off-by: Lachlan Roberts <[email protected]>
  • Loading branch information
lachlan-roberts committed Apr 18, 2023
1 parent 09e6e6b commit a7b0b72
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

package org.eclipse.jetty.http.compression;

/**
* This class contains the Huffman Codes defined in RFC7541.
*/
public class Huffman
{
private Huffman()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,36 @@
import static org.eclipse.jetty.http.compression.Huffman.rowbits;
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 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 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)
throw new IllegalStateException();
_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++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@
import static org.eclipse.jetty.http.compression.Huffman.CODES;
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
{
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;
Expand All @@ -42,21 +56,37 @@ public static int octetsNeeded(byte[] b)
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 buffer the buffer to encode into.
* @param b the byte array to encode.
*/
public static void encode(ByteBuffer buffer, byte[] 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)
{
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@

import java.nio.ByteBuffer;

/**
* Used to encode integers as described in RFC7541.
*/
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)
{
if (n == 8)
Expand All @@ -43,6 +55,12 @@ public static int octetsNeeded(int n, long i)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,49 @@

import java.nio.ByteBuffer;

/**
* Used to decode integers as described in RFC7541.
*/
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 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)
throw new IllegalStateException();
_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)
Expand Down Expand Up @@ -86,6 +95,9 @@ public long decodeLong(ByteBuffer buffer)
}
}

/**
* Reset the internal state of the parser.
*/
public void reset()
{
_prefix = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@

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
{
private final NBitIntegerParser _integerParser;
Expand All @@ -43,13 +53,27 @@ public NBitStringParser()
_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)
{
if (_state != State.PARSING)
throw new IllegalStateException();
_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)
Expand Down
17 changes: 15 additions & 2 deletions jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
Expand All @@ -34,6 +35,18 @@

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()
{
return Stream.of(
Expand Down Expand Up @@ -94,7 +107,7 @@ public static Stream<Arguments> testDecode8859OnlyArguments()
public void testDecode8859Only(String hexString, char expected) throws Exception
{
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(hexString));
String decoded = HuffmanDecoder.decode(buffer, buffer.remaining());
String decoded = decode(buffer, buffer.remaining());
assertThat(decoded, equalTo("" + expected));
}

Expand Down Expand Up @@ -146,6 +159,6 @@ private ByteBuffer encode(String s)

private String decode(ByteBuffer buffer) throws Exception
{
return HuffmanDecoder.decode(buffer, buffer.remaining());
return decode(buffer, buffer.remaining());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class HpackDecoder
private final HpackContext _context;
private final MetaDataBuilder _builder;
private final HuffmanDecoder _huffmanDecoder;
private final NBitIntegerParser _integerParser;
private int _localMaxDynamicTableSize;

/**
Expand All @@ -53,6 +54,7 @@ public HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize)
_localMaxDynamicTableSize = localMaxDynamicTableSize;
_builder = new MetaDataBuilder(maxHeaderSize);
_huffmanDecoder = new HuffmanDecoder();
_integerParser = new NBitIntegerParser();
}

public HpackContext getHpackContext()
Expand Down Expand Up @@ -277,14 +279,25 @@ private int integerDecode(ByteBuffer buffer, int prefix) throws HpackException.C
{
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)
{
HpackException.CompressionException compressionException = new HpackException.CompressionException(e.getMessage());
compressionException.initCause(e);
throw compressionException;
}
finally
{
_integerParser.reset();
}
}

private String huffmanDecode(ByteBuffer buffer, int length) throws HpackException.CompressionException
Expand Down
Loading

0 comments on commit a7b0b72

Please sign in to comment.