backport the jetty-http Huffman encoders/decoders from 10.0.x (#10546)
* backport the jetty-http Huffman encoders/decoders from 10.0.x * fix some hpack tests after changes Signed-off-by: Lachlan Roberts <lachlan@webtide.com> Co-authored-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
parent
b9d3213898
commit
c7a4b05fc7
|
@ -188,5 +188,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.
|
||||
* <pre>
|
||||
* field-vchar = VCHAR / obs-text
|
||||
* obs-text = %x80-FF
|
||||
* VCHAR = %x21-7E
|
||||
* </pre>
|
||||
* @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 < ' ');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
public class EncodingException extends Exception
|
||||
{
|
||||
public EncodingException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -16,14 +16,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
|
||||
|
@ -291,7 +293,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;
|
||||
|
@ -307,9 +309,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];
|
||||
|
@ -352,200 +354,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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* <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
|
||||
{
|
||||
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)
|
||||
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++)
|
||||
{
|
||||
if (!buffer.hasRemaining())
|
||||
return null;
|
||||
|
||||
int b = buffer.get() & 0xFF;
|
||||
_current = (_current << 8) | b;
|
||||
_bits += 8;
|
||||
while (_bits >= 8)
|
||||
{
|
||||
int i = (_current >>> (_bits - 8)) & 0xFF;
|
||||
_node = Huffman.tree[_node * 256 + i];
|
||||
if (rowbits[_node] != 0)
|
||||
{
|
||||
if (rowsym[_node] == Huffman.EOS)
|
||||
{
|
||||
reset();
|
||||
throw new EncodingException("eos_in_content");
|
||||
}
|
||||
|
||||
// terminal node
|
||||
char c = rowsym[_node];
|
||||
c = HttpTokens.sanitizeFieldVchar(c);
|
||||
_builder.append((byte)c);
|
||||
_bits -= rowbits[_node];
|
||||
_node = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// non-terminal node
|
||||
_bits -= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (_bits > 0)
|
||||
{
|
||||
int i = (_current << (8 - _bits)) & 0xFF;
|
||||
int lastNode = _node;
|
||||
_node = Huffman.tree[_node * 256 + i];
|
||||
|
||||
if (rowbits[_node] == 0 || rowbits[_node] > _bits)
|
||||
{
|
||||
int requiredPadding = 0;
|
||||
for (int j = 0; j < _bits; j++)
|
||||
{
|
||||
requiredPadding = (requiredPadding << 1) | 1;
|
||||
}
|
||||
|
||||
if ((i >> (8 - _bits)) != requiredPadding)
|
||||
throw new EncodingException("incorrect_padding");
|
||||
|
||||
_node = lastNode;
|
||||
break;
|
||||
}
|
||||
|
||||
char c = rowsym[_node];
|
||||
c = HttpTokens.sanitizeFieldVchar(c);
|
||||
_builder.append((byte)c);
|
||||
_bits -= rowbits[_node];
|
||||
_node = 0;
|
||||
}
|
||||
|
||||
if (_node != 0)
|
||||
{
|
||||
reset();
|
||||
throw new EncodingException("bad_termination");
|
||||
}
|
||||
|
||||
String value = _builder.build();
|
||||
reset();
|
||||
return value;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
_builder.reset();
|
||||
_count = 0;
|
||||
_current = 0;
|
||||
_node = 0;
|
||||
_bits = 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* <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;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
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)
|
||||
{
|
||||
if (!buffer.hasRemaining())
|
||||
return -1;
|
||||
|
||||
_started = true;
|
||||
_multiplier = 1;
|
||||
int nbits = 0xFF >>> (8 - _prefix);
|
||||
_total = buffer.get() & nbits;
|
||||
if (_total < nbits)
|
||||
{
|
||||
long total = _total;
|
||||
reset();
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
// If we have no more remaining we return -1 to indicate that more data is needed to continue parsing.
|
||||
if (!buffer.hasRemaining())
|
||||
return -1;
|
||||
|
||||
int b = buffer.get() & 0xFF;
|
||||
_total = Math.addExact(_total, (b & 127) * _multiplier);
|
||||
_multiplier = Math.multiplyExact(_multiplier, 128);
|
||||
if ((b & 128) == 0)
|
||||
{
|
||||
long total = _total;
|
||||
reset();
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the internal state of the parser.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
_prefix = 0;
|
||||
_total = 0;
|
||||
_multiplier = 1;
|
||||
_started = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Used to encode integers as described in RFC7541.
|
||||
*/
|
||||
public class NBitIntegerEncoder
|
||||
{
|
||||
private NBitIntegerEncoder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param prefix the prefix used to encode this long.
|
||||
* @param value the integer to encode.
|
||||
* @return the number of octets it would take to encode the long.
|
||||
*/
|
||||
public static int octetsNeeded(int prefix, long value)
|
||||
{
|
||||
if (prefix <= 0 || prefix > 8)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
int nbits = 0xFF >>> (8 - prefix);
|
||||
value = value - nbits;
|
||||
if (value < 0)
|
||||
return 1;
|
||||
if (value == 0)
|
||||
return 2;
|
||||
int lz = Long.numberOfLeadingZeros(value);
|
||||
int log = 64 - lz;
|
||||
|
||||
// The return value is 1 for the prefix + the number of 7-bit groups necessary to encode the value.
|
||||
return 1 + (log + 6) / 7;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param buffer the buffer to encode into.
|
||||
* @param prefix the prefix used to encode this long.
|
||||
* @param value the long to encode into the buffer.
|
||||
*/
|
||||
public static void encode(ByteBuffer buffer, int prefix, long value)
|
||||
{
|
||||
if (prefix <= 0 || prefix > 8)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
// If prefix is 8 we add an empty byte as we initially modify last byte from the buffer.
|
||||
if (prefix == 8)
|
||||
buffer.put((byte)0x00);
|
||||
|
||||
int bits = 0xFF >>> (8 - prefix);
|
||||
int p = buffer.position() - 1;
|
||||
if (value < bits)
|
||||
{
|
||||
buffer.put(p, (byte)((buffer.get(p) & ~bits) | value));
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.put(p, (byte)(buffer.get(p) | bits));
|
||||
long length = value - bits;
|
||||
while (true)
|
||||
{
|
||||
// The value of ~0x7F is different to 0x80 because of all the 1s from the MSB.
|
||||
if ((length & ~0x7FL) == 0)
|
||||
{
|
||||
buffer.put((byte)length);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.put((byte)((length & 0x7F) | 0x80));
|
||||
length >>>= 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
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 NBitStringDecoder
|
||||
{
|
||||
private final NBitIntegerDecoder _integerDecoder;
|
||||
private final HuffmanDecoder _huffmanBuilder;
|
||||
private final CharsetStringBuilder.Iso88591StringBuilder _builder;
|
||||
private boolean _huffman;
|
||||
private int _count;
|
||||
private int _length;
|
||||
private int _prefix;
|
||||
|
||||
private State _state = State.PARSING;
|
||||
|
||||
private enum State
|
||||
{
|
||||
PARSING,
|
||||
LENGTH,
|
||||
VALUE
|
||||
}
|
||||
|
||||
public NBitStringDecoder()
|
||||
{
|
||||
_integerDecoder = new NBitIntegerDecoder();
|
||||
_huffmanBuilder = new HuffmanDecoder();
|
||||
_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)
|
||||
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)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case PARSING:
|
||||
byte firstByte = buffer.get(buffer.position());
|
||||
_huffman = ((0x80 >>> (8 - _prefix)) & firstByte) != 0;
|
||||
_state = State.LENGTH;
|
||||
_integerDecoder.setPrefix(_prefix - 1);
|
||||
continue;
|
||||
|
||||
case LENGTH:
|
||||
_length = _integerDecoder.decodeInt(buffer);
|
||||
if (_length < 0)
|
||||
return null;
|
||||
_state = State.VALUE;
|
||||
_huffmanBuilder.setLength(_length);
|
||||
continue;
|
||||
|
||||
case VALUE:
|
||||
String value = _huffman ? _huffmanBuilder.decode(buffer) : stringDecode(buffer);
|
||||
if (value != null)
|
||||
reset();
|
||||
return value;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(_state.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String stringDecode(ByteBuffer buffer)
|
||||
{
|
||||
for (; _count < _length; _count++)
|
||||
{
|
||||
if (!buffer.hasRemaining())
|
||||
return null;
|
||||
_builder.append(buffer.get());
|
||||
}
|
||||
|
||||
return _builder.build();
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
_state = State.PARSING;
|
||||
_integerDecoder.reset();
|
||||
_huffmanBuilder.reset();
|
||||
_builder.reset();
|
||||
_prefix = 0;
|
||||
_count = 0;
|
||||
_length = 0;
|
||||
_huffman = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.compression;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http.HttpTokens;
|
||||
|
||||
public class NBitStringEncoder
|
||||
{
|
||||
private NBitStringEncoder()
|
||||
{
|
||||
}
|
||||
|
||||
public static int octetsNeeded(int prefix, String value, boolean huffman)
|
||||
{
|
||||
if (prefix <= 0 || prefix > 8)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
int contentPrefix = (prefix == 1) ? 8 : prefix - 1;
|
||||
int encodedValueSize = huffman ? HuffmanEncoder.octetsNeeded(value) : value.length();
|
||||
int encodedLengthSize = NBitIntegerEncoder.octetsNeeded(contentPrefix, encodedValueSize);
|
||||
|
||||
// If prefix was 1, then we count an extra byte needed for the prefix.
|
||||
return encodedLengthSize + encodedValueSize + (prefix == 1 ? 1 : 0);
|
||||
}
|
||||
|
||||
public static void encode(ByteBuffer buffer, int prefix, String value, boolean huffman)
|
||||
{
|
||||
if (prefix <= 0 || prefix > 8)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
byte huffmanFlag = huffman ? (byte)(0x01 << (prefix - 1)) : (byte)0x00;
|
||||
if (prefix == 8)
|
||||
{
|
||||
buffer.put(huffmanFlag);
|
||||
}
|
||||
else
|
||||
{
|
||||
int p = buffer.position() - 1;
|
||||
buffer.put(p, (byte)(buffer.get(p) | huffmanFlag));
|
||||
}
|
||||
|
||||
// Start encoding size & content in rest of prefix.
|
||||
// If prefix was 1 we set it back to 8 to indicate to start on a new byte.
|
||||
prefix = (prefix == 1) ? 8 : prefix - 1;
|
||||
|
||||
if (huffman)
|
||||
{
|
||||
int encodedValueSize = HuffmanEncoder.octetsNeeded(value);
|
||||
NBitIntegerEncoder.encode(buffer, prefix, encodedValueSize);
|
||||
HuffmanEncoder.encode(buffer, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
int encodedValueSize = value.length();
|
||||
NBitIntegerEncoder.encode(buffer, prefix, encodedValueSize);
|
||||
for (int i = 0; i < encodedValueSize; i++)
|
||||
{
|
||||
char c = value.charAt(i);
|
||||
c = HttpTokens.sanitizeFieldVchar(c);
|
||||
buffer.put((byte)c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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.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<Arguments> 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<Arguments> 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(TypeUtil.fromHexString(hexString));
|
||||
String decoded = decode(buffer, buffer.remaining());
|
||||
assertThat(decoded, equalTo("" + expected));
|
||||
}
|
||||
|
||||
public static Stream<Arguments> 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());
|
||||
}
|
||||
}
|
|
@ -16,33 +16,37 @@
|
|||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http2.hpack;
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
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;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@SuppressWarnings("PointlessArithmeticExpression")
|
||||
public class NBitIntegerTest
|
||||
{
|
||||
private final NBitIntegerDecoder _decoder = new NBitIntegerDecoder();
|
||||
|
||||
@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(1, NBitIntegerEncoder.octetsNeeded(5, 10));
|
||||
assertEquals(3, NBitIntegerEncoder.octetsNeeded(5, 1337));
|
||||
assertEquals(1, NBitIntegerEncoder.octetsNeeded(8, 42));
|
||||
assertEquals(3, NBitIntegerEncoder.octetsNeeded(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));
|
||||
assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 62));
|
||||
assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 63));
|
||||
assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 64));
|
||||
assertEquals(3, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x01));
|
||||
assertEquals(4, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80));
|
||||
assertEquals(5, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -83,12 +87,12 @@ public class NBitIntegerTest
|
|||
int p = BufferUtil.flipToFill(buf);
|
||||
if (n < 8)
|
||||
buf.put((byte)0x00);
|
||||
NBitInteger.encode(buf, n, i);
|
||||
NBitIntegerEncoder.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));
|
||||
assertEquals(expected.length() / 2, NBitIntegerEncoder.octetsNeeded(n, i));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -126,8 +130,8 @@ public class NBitIntegerTest
|
|||
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));
|
||||
_decoder.setPrefix(n);
|
||||
assertEquals(expected, _decoder.decodeInt(buf));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -137,7 +141,7 @@ public class NBitIntegerTest
|
|||
int p = BufferUtil.flipToFill(buf);
|
||||
buf.put((byte)0x77);
|
||||
buf.put((byte)0xFF);
|
||||
NBitInteger.encode(buf, 5, 10);
|
||||
NBitIntegerEncoder.encode(buf, 5, 10);
|
||||
BufferUtil.flipToFlush(buf, p);
|
||||
|
||||
String r = TypeUtil.toHexString(BufferUtil.toArray(buf));
|
||||
|
@ -149,9 +153,9 @@ public class NBitIntegerTest
|
|||
public void testDecodeExampleD11()
|
||||
{
|
||||
ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("77EaFF"));
|
||||
buf.position(2);
|
||||
|
||||
assertEquals(10, NBitInteger.decode(buf, 5));
|
||||
buf.position(1);
|
||||
_decoder.setPrefix(5);
|
||||
assertEquals(10, _decoder.decodeInt(buf));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -161,11 +165,10 @@ public class NBitIntegerTest
|
|||
int p = BufferUtil.flipToFill(buf);
|
||||
buf.put((byte)0x88);
|
||||
buf.put((byte)0x00);
|
||||
NBitInteger.encode(buf, 5, 1337);
|
||||
NBitIntegerEncoder.encode(buf, 5, 1337);
|
||||
BufferUtil.flipToFlush(buf, p);
|
||||
|
||||
String r = TypeUtil.toHexString(BufferUtil.toArray(buf));
|
||||
|
||||
assertEquals("881f9a0a", r);
|
||||
}
|
||||
|
||||
|
@ -173,9 +176,9 @@ public class NBitIntegerTest
|
|||
public void testDecodeExampleD12()
|
||||
{
|
||||
ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("881f9a0aff"));
|
||||
buf.position(2);
|
||||
|
||||
assertEquals(1337, NBitInteger.decode(buf, 5));
|
||||
buf.position(1);
|
||||
_decoder.setPrefix(5);
|
||||
assertEquals(1337, _decoder.decodeInt(buf));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -185,7 +188,7 @@ public class NBitIntegerTest
|
|||
int p = BufferUtil.flipToFill(buf);
|
||||
buf.put((byte)0x88);
|
||||
buf.put((byte)0xFF);
|
||||
NBitInteger.encode(buf, 8, 42);
|
||||
NBitIntegerEncoder.encode(buf, 8, 42);
|
||||
BufferUtil.flipToFlush(buf, p);
|
||||
|
||||
String r = TypeUtil.toHexString(BufferUtil.toArray(buf));
|
||||
|
@ -198,7 +201,7 @@ public class NBitIntegerTest
|
|||
{
|
||||
ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("882aFf"));
|
||||
buf.position(1);
|
||||
|
||||
assertEquals(42, NBitInteger.decode(buf, 8));
|
||||
_decoder.setPrefix(8);
|
||||
assertEquals(42, _decoder.decodeInt(buf));
|
||||
}
|
||||
}
|
|
@ -29,6 +29,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.ArrayTernaryTrie;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.Trie;
|
||||
|
@ -153,7 +155,7 @@ public class HpackContext
|
|||
|
||||
case C_STATUS:
|
||||
{
|
||||
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, Integer.valueOf(value)));
|
||||
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, value));
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -461,21 +463,21 @@ public class HpackContext
|
|||
super(field);
|
||||
_slot = index;
|
||||
String value = field.getValue();
|
||||
if (value != null && value.length() > 0)
|
||||
if (value != null && !value.isEmpty())
|
||||
{
|
||||
int huffmanLen = Huffman.octetsNeeded(value);
|
||||
int huffmanLen = HuffmanEncoder.octetsNeeded(value);
|
||||
if (huffmanLen < 0)
|
||||
throw new IllegalStateException("bad value");
|
||||
int lenLen = NBitInteger.octectsNeeded(7, huffmanLen);
|
||||
_huffmanValue = new byte[1 + lenLen + huffmanLen];
|
||||
int lenLen = NBitIntegerEncoder.octetsNeeded(7, huffmanLen);
|
||||
_huffmanValue = new byte[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;
|
||||
|
|
|
@ -24,8 +24,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.TypeUtil;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.CharsetStringBuilder;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
|
@ -41,6 +45,8 @@ public class HpackDecoder
|
|||
|
||||
private final HpackContext _context;
|
||||
private final MetaDataBuilder _builder;
|
||||
private final HuffmanDecoder _huffmanDecoder;
|
||||
private final NBitIntegerDecoder _integerDecoder;
|
||||
private int _localMaxDynamicTableSize;
|
||||
|
||||
/**
|
||||
|
@ -52,6 +58,8 @@ public class HpackDecoder
|
|||
_context = new HpackContext(localMaxDynamicTableSize);
|
||||
_localMaxDynamicTableSize = localMaxDynamicTableSize;
|
||||
_builder = new MetaDataBuilder(maxHeaderSize);
|
||||
_huffmanDecoder = new HuffmanDecoder();
|
||||
_integerDecoder = new NBitIntegerDecoder();
|
||||
}
|
||||
|
||||
public HpackContext getHpackContext()
|
||||
|
@ -69,27 +77,22 @@ 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 (buffer.remaining() > _builder.getMaxSize())
|
||||
throw new HpackException.SessionException("431 Request Header Fields too large");
|
||||
// If the buffer is larger than the max headers size, don't even start decoding it.
|
||||
int maxSize = _builder.getMaxSize();
|
||||
if (maxSize > 0 && buffer.remaining() > maxSize)
|
||||
throw new HpackException.SessionException("Header fields size too large");
|
||||
|
||||
boolean emitted = false;
|
||||
|
||||
while (buffer.hasRemaining())
|
||||
{
|
||||
if (LOG.isDebugEnabled() && buffer.hasArray())
|
||||
{
|
||||
int l = Math.min(buffer.remaining(), 32);
|
||||
LOG.debug("decode {}{}",
|
||||
TypeUtil.toHexString(buffer.array(), buffer.arrayOffset() + buffer.position(), l),
|
||||
l < buffer.remaining() ? "..." : "");
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("decode {}", BufferUtil.toHexString(buffer));
|
||||
|
||||
byte b = buffer.get();
|
||||
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);
|
||||
|
@ -130,11 +133,11 @@ 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);
|
||||
LOG.debug("decode resize={}", size);
|
||||
if (size > _localMaxDynamicTableSize)
|
||||
throw new IllegalArgumentException();
|
||||
throw new HpackException.CompressionException("Dynamic table resize exceeded max limit");
|
||||
if (emitted)
|
||||
throw new HpackException.CompressionException("Dynamic table resize after fields");
|
||||
_context.resize(size);
|
||||
|
@ -143,7 +146,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
|
||||
|
@ -151,7 +154,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:
|
||||
|
@ -170,12 +173,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; )
|
||||
{
|
||||
|
@ -211,12 +213,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;
|
||||
|
@ -277,19 +278,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);
|
||||
int position = buffer.position();
|
||||
int start = buffer.arrayOffset() + position;
|
||||
int end = start + length;
|
||||
buffer.position(position + length);
|
||||
byte[] array = buffer.array();
|
||||
for (int i = start; i < end; i++)
|
||||
try
|
||||
{
|
||||
builder.append((char)(0x7f & array[i]));
|
||||
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;
|
||||
}
|
||||
return builder.toString();
|
||||
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(HttpTokens.sanitizeFieldVchar((char)buffer.get()));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,7 +19,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;
|
||||
|
@ -34,10 +33,13 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
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.http.compression.NBitStringEncoder;
|
||||
import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
|
||||
import org.eclipse.jetty.http2.hpack.HpackContext.StaticEntry;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.TypeUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
|
@ -255,13 +257,9 @@ public class HpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
// Check size
|
||||
if (_maxHeaderListSize > 0 && _headerListSize > _maxHeaderListSize)
|
||||
{
|
||||
LOG.warn("Header list size too large {} > {} for {}", _headerListSize, _maxHeaderListSize);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("metadata={}", metadata);
|
||||
}
|
||||
int maxHeaderListSize = getMaxHeaderListSize();
|
||||
if (maxHeaderListSize > 0 && _headerListSize > maxHeaderListSize)
|
||||
throw new HpackException.SessionException("Header size %d > %d", _headerListSize, maxHeaderListSize);
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug(String.format("CtxTbl[%x] encoded %d octets", _context.hashCode(), buffer.position() - pos));
|
||||
|
@ -278,13 +276,11 @@ public class HpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxDynamicTableSize)
|
||||
public void encodeMaxDynamicTableSize(ByteBuffer buffer, int maxTableSize)
|
||||
{
|
||||
if (maxDynamicTableSize > _remoteMaxDynamicTableSize)
|
||||
throw new IllegalArgumentException();
|
||||
buffer.put((byte)0x20);
|
||||
NBitInteger.encode(buffer, 5, maxDynamicTableSize);
|
||||
_context.resize(maxDynamicTableSize);
|
||||
NBitIntegerEncoder.encode(buffer, 5, maxTableSize);
|
||||
_context.resize(maxTableSize);
|
||||
}
|
||||
|
||||
public void encode(ByteBuffer buffer, HttpField field)
|
||||
|
@ -295,8 +291,6 @@ public class HpackEncoder
|
|||
int fieldSize = field.getName().length() + field.getValue().length();
|
||||
_headerListSize += fieldSize + 32;
|
||||
|
||||
final int p = _debug ? buffer.position() : -1;
|
||||
|
||||
String encoding = null;
|
||||
|
||||
// Is there an index entry for the field?
|
||||
|
@ -314,9 +308,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" : "") + NBitIntegerEncoder.octetsNeeded(7, index);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -390,19 +384,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
|
||||
|
@ -413,7 +407,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");
|
||||
}
|
||||
}
|
||||
|
@ -425,10 +419,8 @@ public class HpackEncoder
|
|||
|
||||
if (_debug)
|
||||
{
|
||||
byte[] bytes = new byte[buffer.position() - p];
|
||||
buffer.position(p);
|
||||
buffer.get(bytes);
|
||||
LOG.debug("encode {}:'{}' to '{}'", encoding, field, TypeUtil.toHexString(bytes));
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString((ByteBuffer)buffer.duplicate().flip()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,55 +432,17 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
static void encodeValue(ByteBuffer buffer, boolean huffman, String value)
|
||||
{
|
||||
if (huffman)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// add literal assuming iso_8859_1
|
||||
buffer.put((byte)0x00).mark();
|
||||
NBitInteger.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;
|
||||
}
|
||||
buffer.put((byte)c);
|
||||
}
|
||||
}
|
||||
NBitStringEncoder.encode(buffer, 8, value, huffman);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
|
||||
package org.eclipse.jetty.http2.hpack;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public abstract class HpackException extends Exception
|
||||
{
|
||||
HpackException(String messageFormat, Object... args)
|
||||
|
@ -35,7 +34,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);
|
||||
}
|
||||
|
@ -48,7 +47,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);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,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;
|
||||
|
||||
/**
|
||||
|
@ -31,18 +33,12 @@ import org.eclipse.jetty.util.BufferUtil;
|
|||
public class HpackFieldPreEncoder implements HttpFieldPreEncoder
|
||||
{
|
||||
|
||||
/**
|
||||
* @see org.eclipse.jetty.http.HttpFieldPreEncoder#getHttpVersion()
|
||||
*/
|
||||
@Override
|
||||
public HttpVersion getHttpVersion()
|
||||
{
|
||||
return HttpVersion.HTTP_2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.eclipse.jetty.http.HttpFieldPreEncoder#getEncodedField(org.eclipse.jetty.http.HttpHeader, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public byte[] getEncodedField(HttpHeader header, String name, String value)
|
||||
{
|
||||
|
@ -78,12 +74,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);
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.eclipse.jetty.http2.hpack.HpackException.SessionException;
|
|||
|
||||
public class MetaDataBuilder
|
||||
{
|
||||
private final int _maxSize;
|
||||
private int _maxSize;
|
||||
private int _size;
|
||||
private Integer _status;
|
||||
private String _method;
|
||||
|
@ -60,6 +60,11 @@ public class MetaDataBuilder
|
|||
return _maxSize;
|
||||
}
|
||||
|
||||
public void setMaxSize(int maxSize)
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size.
|
||||
*
|
||||
|
@ -70,17 +75,18 @@ 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");
|
||||
if (name == null || name.isEmpty())
|
||||
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);
|
||||
int maxSize = getMaxSize();
|
||||
if (maxSize > 0 && _size > maxSize)
|
||||
throw new SessionException("Header size %d > %d", _size, maxSize);
|
||||
|
||||
if (field instanceof StaticTableHttpField)
|
||||
{
|
||||
|
@ -89,7 +95,7 @@ public class MetaDataBuilder
|
|||
{
|
||||
case C_STATUS:
|
||||
if (checkPseudoHeader(header, _status))
|
||||
_status = (Integer)staticField.getStaticValue();
|
||||
_status = staticField.getIntValue();
|
||||
_response = true;
|
||||
break;
|
||||
|
||||
|
@ -157,7 +163,7 @@ public class MetaDataBuilder
|
|||
case C_PATH:
|
||||
if (checkPseudoHeader(header, _path))
|
||||
{
|
||||
if (value != null && value.length() > 0)
|
||||
if (value != null && !value.isEmpty())
|
||||
_path = value;
|
||||
else
|
||||
streamException("No Path");
|
||||
|
@ -201,7 +207,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)
|
||||
|
@ -267,23 +273,7 @@ public class MetaDataBuilder
|
|||
_authority = null;
|
||||
_path = 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,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;
|
||||
|
@ -37,6 +40,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()
|
||||
|
@ -428,10 +457,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);
|
||||
}
|
||||
|
|
|
@ -470,7 +470,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 */
|
||||
|
@ -483,7 +483,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 */
|
||||
|
@ -496,7 +496,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
|
||||
|
@ -508,7 +508,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
|
||||
|
@ -520,7 +520,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
|
||||
|
|
|
@ -133,7 +133,7 @@ public class HpackTest
|
|||
}
|
||||
catch (HpackException.SessionException e)
|
||||
{
|
||||
assertThat(e.getMessage(), containsString("Header too large"));
|
||||
assertThat(e.getMessage(), containsString("Header size 198 > 164"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,21 +141,22 @@ public class HpackTest
|
|||
public void encodeDecodeNonAscii() throws Exception
|
||||
{
|
||||
HpackEncoder encoder = new HpackEncoder();
|
||||
HpackDecoder decoder = new HpackDecoder(4096, 8192);
|
||||
ByteBuffer buffer = BufferUtil.allocate(16 * 1024);
|
||||
|
||||
HttpFields fields0 = new HttpFields();
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
|
||||
fields0.add("Cookie", "[\uD842\uDF9F]");
|
||||
fields0.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
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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<Arguments> 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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* <p>Build a string from a sequence of bytes and/or characters.</p>
|
||||
* <p>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.</p>
|
||||
* <p>Any coding errors in the string will be reported by a {@link CharacterCodingException} thrown
|
||||
* from the {@link #build()} method.</p>
|
||||
* @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
|
||||
{
|
||||
/**
|
||||
* @param b An encoded byte to append
|
||||
*/
|
||||
void append(byte b);
|
||||
|
||||
/**
|
||||
* @param c A decoded character to append
|
||||
*/
|
||||
void append(char c);
|
||||
|
||||
/**
|
||||
* @param bytes Array of encoded bytes to append
|
||||
*/
|
||||
default void append(byte[] bytes)
|
||||
{
|
||||
append(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param b Array of encoded bytes
|
||||
* @param offset offset into the array
|
||||
* @param length the number of bytes to append from the array.
|
||||
*/
|
||||
default void append(byte[] b, int offset, int length)
|
||||
{
|
||||
int end = offset + length;
|
||||
for (int i = offset; i < end; i++)
|
||||
append(b[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param chars sequence of decoded characters
|
||||
* @param offset offset into the array
|
||||
* @param length the number of character to append from the sequence.
|
||||
*/
|
||||
default void append(CharSequence chars, int offset, int length)
|
||||
{
|
||||
int end = offset + length;
|
||||
for (int i = offset; i < end; i++)
|
||||
append(chars.charAt(i));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param buf Buffer of encoded bytes to append. The bytes are consumed from the buffer.
|
||||
*/
|
||||
default void append(ByteBuffer buf)
|
||||
{
|
||||
int end = buf.position() + buf.remaining();
|
||||
while (buf.position() < end)
|
||||
append(buf.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Build the completed string and reset the buffer.</p>
|
||||
* @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();
|
||||
|
||||
/**
|
||||
* @param charset The charset
|
||||
* @return A {@link CharsetStringBuilder} suitable for the charset.
|
||||
*/
|
||||
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, 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, 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));
|
||||
_buffer.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
|
||||
_buffer.flip();
|
||||
_stringBuilder.append(_decoder.decode(_buffer));
|
||||
_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
|
||||
_buffer.flip();
|
||||
_stringBuilder.append(_decoder.decode(_buffer));
|
||||
_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)
|
||||
{
|
||||
_buffer.flip();
|
||||
CharSequence decoded = _decoder.decode(_buffer);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -107,6 +107,27 @@ public class StringUtil
|
|||
'\170', '\171', '\172', '\173', '\174', '\175', '\176', '\177'
|
||||
};
|
||||
|
||||
// @checkstyle-disable-check : IllegalTokenTextCheck
|
||||
private static final char[] uppercases =
|
||||
{
|
||||
'\000', '\001', '\002', '\003', '\004', '\005', '\006', '\007',
|
||||
'\010', '\011', '\012', '\013', '\014', '\015', '\016', '\017',
|
||||
'\020', '\021', '\022', '\023', '\024', '\025', '\026', '\027',
|
||||
'\030', '\031', '\032', '\033', '\034', '\035', '\036', '\037',
|
||||
'\040', '\041', '\042', '\043', '\044', '\045', '\046', '\047',
|
||||
'\050', '\051', '\052', '\053', '\054', '\055', '\056', '\057',
|
||||
'\060', '\061', '\062', '\063', '\064', '\065', '\066', '\067',
|
||||
'\070', '\071', '\072', '\073', '\074', '\075', '\076', '\077',
|
||||
'\100', '\101', '\102', '\103', '\104', '\105', '\106', '\107',
|
||||
'\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117',
|
||||
'\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127',
|
||||
'\130', '\131', '\132', '\133', '\134', '\135', '\136', '\137',
|
||||
'\140', '\101', '\102', '\103', '\104', '\105', '\106', '\107',
|
||||
'\110', '\111', '\112', '\113', '\114', '\115', '\116', '\117',
|
||||
'\120', '\121', '\122', '\123', '\124', '\125', '\126', '\127',
|
||||
'\130', '\131', '\132', '\173', '\174', '\175', '\176', '\177'
|
||||
};
|
||||
|
||||
/**
|
||||
* fast lower case conversion. Only works on ascii (not unicode)
|
||||
*
|
||||
|
@ -144,6 +165,42 @@ public class StringUtil
|
|||
return c == null ? s : new String(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* fast upper case conversion. Only works on ascii (not unicode)
|
||||
*
|
||||
* @param s the string to convert
|
||||
* @return a lower case version of s
|
||||
*/
|
||||
public static String asciiToUpperCase(String s)
|
||||
{
|
||||
if (s == null)
|
||||
return null;
|
||||
|
||||
char[] c = null;
|
||||
int i = s.length();
|
||||
// look for first conversion
|
||||
while (i-- > 0)
|
||||
{
|
||||
char c1 = s.charAt(i);
|
||||
if (c1 <= 127)
|
||||
{
|
||||
char c2 = uppercases[c1];
|
||||
if (c1 != c2)
|
||||
{
|
||||
c = s.toCharArray();
|
||||
c[i] = c2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (i-- > 0)
|
||||
{
|
||||
if (c[i] <= 127)
|
||||
c[i] = uppercases[c[i]];
|
||||
}
|
||||
return c == null ? s : new String(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all characters from input string that are known to have
|
||||
* special meaning in various filesystems.
|
||||
|
|
Loading…
Reference in New Issue