Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #9554 - move common hpack/qpack code to jetty-http (jetty-10) #9634

Merged
merged 13 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Issue #9554 - add javadoc for huffman / n-bit integer classes and rem…
…ove static decode methods

Signed-off-by: Lachlan Roberts <[email protected]>
  • Loading branch information
lachlan-roberts committed Apr 18, 2023
commit a7b0b727dd842eacfd927d881f0774b7cc83975a
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()
gregw marked this conversation as resolved.
Show resolved Hide resolved
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
lachlan-roberts marked this conversation as resolved.
Show resolved Hide resolved
{
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
gregw marked this conversation as resolved.
Show resolved Hide resolved
{
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