Use ISO-8859-1 for encoding/decoding in huffman/hpack/qpack

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2023-04-17 17:11:14 +10:00
parent b6d89af3ea
commit c3b6b47915
18 changed files with 495 additions and 369 deletions

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.http;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.Objects;
import java.util.function.Supplier; import java.util.function.Supplier;
public class MetaData implements Iterable<HttpField> public class MetaData implements Iterable<HttpField>
@ -100,6 +101,28 @@ public class MetaData implements Iterable<HttpField>
return _fields.iterator(); return _fields.iterator();
} }
@Override
public int hashCode()
{
return Objects.hash(_httpVersion, _contentLength, _fields, _trailerSupplier);
}
@Override
public boolean equals(Object obj)
{
if (!(obj instanceof MetaData))
return false;
MetaData other = (MetaData)obj;
if (!Objects.equals(_httpVersion, other._httpVersion))
return false;
if (!Objects.equals(_contentLength, other._contentLength))
return false;
if (!Objects.equals(_fields, other._fields))
return false;
return _trailerSupplier == null && other._trailerSupplier == null;
}
@Override @Override
public String toString() public String toString()
{ {

View File

@ -346,4 +346,47 @@ public class Huffman
} }
} }
} }
public static boolean isIllegalCharacter(char c)
{
return (c >= 256 || c < ' ');
}
public static char getReplacementCharacter(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 (c >= 256 || c < ' ')
return '?';
}
return c;
}
public static int getReplacementCharacter(int i)
{
switch (i)
{
// 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 (i >= 256 || i < ' ')
return '?';
}
return i;
}
} }

View File

@ -15,7 +15,7 @@ package org.eclipse.jetty.http.compression;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.CharsetStringBuilder;
import static org.eclipse.jetty.http.compression.Huffman.rowbits; import static org.eclipse.jetty.http.compression.Huffman.rowbits;
import static org.eclipse.jetty.http.compression.Huffman.rowsym; import static org.eclipse.jetty.http.compression.Huffman.rowsym;
@ -34,7 +34,7 @@ public class HuffmanDecoder
return decoded; return decoded;
} }
private final Utf8StringBuilder _utf8 = new Utf8StringBuilder(); private final CharsetStringBuilder.Iso8859StringBuilder _builder = new CharsetStringBuilder.Iso8859StringBuilder();
private int _length = 0; private int _length = 0;
private int _count = 0; private int _count = 0;
private int _node = 0; private int _node = 0;
@ -71,7 +71,9 @@ public class HuffmanDecoder
} }
// terminal node // terminal node
_utf8.append((byte)(0xFF & rowsym[_node])); int i = 0xFF & rowsym[_node];
i = Huffman.getReplacementCharacter(i);
_builder.append((byte)i);
_bits -= rowbits[_node]; _bits -= rowbits[_node];
_node = 0; _node = 0;
} }
@ -104,7 +106,9 @@ public class HuffmanDecoder
break; break;
} }
_utf8.append((byte)(0xFF & rowsym[_node])); int i = 0xFF & rowsym[_node];
i = Huffman.getReplacementCharacter(i);
_builder.append((byte)i);
_bits -= rowbits[_node]; _bits -= rowbits[_node];
_node = 0; _node = 0;
} }
@ -115,14 +119,14 @@ public class HuffmanDecoder
throw new EncodingException("bad_termination"); throw new EncodingException("bad_termination");
} }
String value = _utf8.toString(); String value = _builder.build();
reset(); reset();
return value; return value;
} }
public void reset() public void reset()
{ {
_utf8.reset(); _builder.reset();
_count = 0; _count = 0;
_current = 0; _current = 0;
_node = 0; _node = 0;

View File

@ -50,12 +50,12 @@ public class HuffmanEncoder
encode(CODES, buffer, b); encode(CODES, buffer, b);
} }
public static int octetsNeededLC(String s) public static int octetsNeededLowercase(String s)
{ {
return octetsNeeded(LCCODES, s); return octetsNeeded(LCCODES, s);
} }
public static void encodeLC(ByteBuffer buffer, String s) public static void encodeLowercase(ByteBuffer buffer, String s)
{ {
encode(LCCODES, buffer, s); encode(LCCODES, buffer, s);
} }
@ -67,7 +67,7 @@ public class HuffmanEncoder
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
char c = s.charAt(i); char c = s.charAt(i);
if (c >= 128 || c < ' ') if (Huffman.isIllegalCharacter(c))
return -1; return -1;
needed += table[c][1]; needed += table[c][1];
} }
@ -88,7 +88,7 @@ public class HuffmanEncoder
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {
char c = s.charAt(i); char c = s.charAt(i);
if (c >= 128 || c < ' ') if (Huffman.isIllegalCharacter(c))
throw new IllegalArgumentException(); throw new IllegalArgumentException();
int code = table[c][0]; int code = table[c][0];
int bits = table[c][1]; int bits = table[c][1];
@ -119,9 +119,9 @@ public class HuffmanEncoder
for (byte value : b) for (byte value : b)
{ {
int c = 0xFF & value; int i = 0xFF & value;
int code = table[c][0]; int code = table[i][0];
int bits = table[c][1]; int bits = table[i][1];
current <<= bits; current <<= bits;
current |= code; current |= code;

View File

@ -15,11 +15,13 @@ package org.eclipse.jetty.http.compression;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.eclipse.jetty.util.CharsetStringBuilder;
public class NBitStringParser public class NBitStringParser
{ {
private final NBitIntegerParser _integerParser; private final NBitIntegerParser _integerParser;
private final HuffmanDecoder _huffmanBuilder; private final HuffmanDecoder _huffmanBuilder;
private final StringBuilder _stringBuilder; private final CharsetStringBuilder.Iso8859StringBuilder _builder;
private boolean _huffman; private boolean _huffman;
private int _count; private int _count;
private int _length; private int _length;
@ -38,7 +40,7 @@ public class NBitStringParser
{ {
_integerParser = new NBitIntegerParser(); _integerParser = new NBitIntegerParser();
_huffmanBuilder = new HuffmanDecoder(); _huffmanBuilder = new HuffmanDecoder();
_stringBuilder = new StringBuilder(); _builder = new CharsetStringBuilder.Iso8859StringBuilder();
} }
public void setPrefix(int prefix) public void setPrefix(int prefix)
@ -70,7 +72,7 @@ public class NBitStringParser
continue; continue;
case VALUE: case VALUE:
String value = _huffman ? _huffmanBuilder.decode(buffer) : asciiStringDecode(buffer); String value = _huffman ? _huffmanBuilder.decode(buffer) : stringDecode(buffer);
if (value != null) if (value != null)
reset(); reset();
return value; return value;
@ -81,15 +83,16 @@ public class NBitStringParser
} }
} }
private String asciiStringDecode(ByteBuffer buffer) private String stringDecode(ByteBuffer buffer)
{ {
for (; _count < _length; _count++) for (; _count < _length; _count++)
{ {
if (!buffer.hasRemaining()) if (!buffer.hasRemaining())
return null; return null;
_stringBuilder.append((char)(0x7F & buffer.get())); _builder.append(buffer.get());
} }
return _stringBuilder.toString();
return _builder.build();
} }
public void reset() public void reset()
@ -97,7 +100,7 @@ public class NBitStringParser
_state = State.PARSING; _state = State.PARSING;
_integerParser.reset(); _integerParser.reset();
_huffmanBuilder.reset(); _huffmanBuilder.reset();
_stringBuilder.setLength(0); _builder.reset();
_prefix = 0; _prefix = 0;
_count = 0; _count = 0;
_length = 0; _length = 0;

View File

@ -11,9 +11,8 @@
// ======================================================================== // ========================================================================
// //
package org.eclipse.jetty.http3.qpack; package org.eclipse.jetty.http;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -21,14 +20,15 @@ import java.util.stream.Stream;
import org.eclipse.jetty.http.compression.HuffmanDecoder; import org.eclipse.jetty.http.compression.HuffmanDecoder;
import org.eclipse.jetty.http.compression.HuffmanEncoder; import org.eclipse.jetty.http.compression.HuffmanEncoder;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.TypeUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -72,15 +72,80 @@ public class HuffmanTest
assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected)); assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected));
} }
@ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name public static Stream<Arguments> testDecode8859OnlyArguments()
@ValueSource(chars = {(char)128, (char)0, (char)-1, ' ' - 1})
public void testEncode8859Only(char bad)
{ {
String s = "bad '" + bad + "'"; return Stream.of(
// These are valid characters for ISO-8859-1.
Arguments.of("FfFe6f", (char)128),
Arguments.of("FfFfFbBf", (char)255),
assertThat(HuffmanEncoder.octetsNeeded(s), Matchers.is(-1)); // RFC9110 specifies these to be replaced as ' ' during decoding.
Arguments.of("FfC7", ' '), // (char)0
Arguments.of("FfFfFfF7", ' '), // '\r'
Arguments.of("FfFfFfF3", ' '), // '\n'
assertThrows(BufferOverflowException.class, // We replace control chars with the default replacement character of '?'.
() -> HuffmanEncoder.encode(BufferUtil.allocate(32), s)); Arguments.of("FfFfFfBf", '?') // (char)(' ' - 1)
);
}
@ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name
@MethodSource("testDecode8859OnlyArguments")
public void testDecode8859Only(String hexString, char expected) throws Exception
{
ByteBuffer buffer = ByteBuffer.wrap(StringUtil.fromHexString(hexString));
String decoded = HuffmanDecoder.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 HuffmanDecoder.decode(buffer, buffer.remaining());
} }
} }

View File

@ -11,7 +11,7 @@
// ======================================================================== // ========================================================================
// //
package org.eclipse.jetty.http3.qpack; package org.eclipse.jetty.http;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;

View File

@ -11,7 +11,7 @@
// ======================================================================== // ========================================================================
// //
package org.eclipse.jetty.http3.qpack; package org.eclipse.jetty.http;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;

View File

@ -24,6 +24,7 @@ import org.eclipse.jetty.http.compression.HuffmanDecoder;
import org.eclipse.jetty.http.compression.NBitIntegerParser; import org.eclipse.jetty.http.compression.NBitIntegerParser;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.CharsetStringBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -39,6 +40,7 @@ public class HpackDecoder
private final HpackContext _context; private final HpackContext _context;
private final MetaDataBuilder _builder; private final MetaDataBuilder _builder;
private final HuffmanDecoder _huffmanDecoder;
private int _localMaxDynamicTableSize; private int _localMaxDynamicTableSize;
/** /**
@ -50,6 +52,7 @@ public class HpackDecoder
_context = new HpackContext(localMaxDynamicTableSize); _context = new HpackContext(localMaxDynamicTableSize);
_localMaxDynamicTableSize = localMaxDynamicTableSize; _localMaxDynamicTableSize = localMaxDynamicTableSize;
_builder = new MetaDataBuilder(maxHeaderSize); _builder = new MetaDataBuilder(maxHeaderSize);
_huffmanDecoder = new HuffmanDecoder();
} }
public HpackContext getHpackContext() public HpackContext getHpackContext()
@ -168,7 +171,7 @@ public class HpackDecoder
if (huffmanName) if (huffmanName)
name = huffmanDecode(buffer, length); name = huffmanDecode(buffer, length);
else else
name = toASCIIString(buffer, length); name = toISO8859String(buffer, length);
check: check:
for (int i = name.length(); i-- > 0; ) for (int i = name.length(); i-- > 0; )
{ {
@ -209,7 +212,7 @@ public class HpackDecoder
if (huffmanValue) if (huffmanValue)
value = huffmanDecode(buffer, length); value = huffmanDecode(buffer, length);
else else
value = toASCIIString(buffer, length); value = toISO8859String(buffer, length);
// Make the new field // Make the new field
HttpField field; HttpField field;
@ -288,7 +291,11 @@ public class HpackDecoder
{ {
try try
{ {
return HuffmanDecoder.decode(buffer, length); _huffmanDecoder.setLength(length);
String decoded = _huffmanDecoder.decode(buffer);
if (decoded == null)
throw new HpackException.CompressionException("invalid string encoding");
return decoded;
} }
catch (EncodingException e) catch (EncodingException e)
{ {
@ -296,16 +303,20 @@ public class HpackDecoder
compressionException.initCause(e); compressionException.initCause(e);
throw compressionException; throw compressionException;
} }
finally
{
_huffmanDecoder.reset();
}
} }
public static String toASCIIString(ByteBuffer buffer, int length) public static String toISO8859String(ByteBuffer buffer, int length)
{ {
StringBuilder builder = new StringBuilder(length); CharsetStringBuilder.Iso8859StringBuilder builder = new CharsetStringBuilder.Iso8859StringBuilder();
for (int i = 0; i < length; ++i) for (int i = 0; i < length; ++i)
{ {
builder.append((char)(0x7F & buffer.get())); builder.append((char)(0x7F & buffer.get()));
} }
return builder.toString(); return builder.build();
} }
@Override @Override

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.http2.hpack; package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
@ -29,6 +28,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.compression.Huffman;
import org.eclipse.jetty.http.compression.HuffmanEncoder; import org.eclipse.jetty.http.compression.HuffmanEncoder;
import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http.compression.NBitIntegerEncoder;
import org.eclipse.jetty.http2.hpack.HpackContext.Entry; import org.eclipse.jetty.http2.hpack.HpackContext.Entry;
@ -441,8 +441,8 @@ public class HpackEncoder
// leave name index bits as 0 // leave name index bits as 0
// Encode the name always with lowercase huffman // Encode the name always with lowercase huffman
buffer.put((byte)0x80); buffer.put((byte)0x80);
NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLC(name)); NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowercase(name));
HuffmanEncoder.encodeLC(buffer, name); HuffmanEncoder.encodeLowercase(buffer, name);
} }
else else
{ {
@ -456,22 +456,11 @@ public class HpackEncoder
{ {
// huffman literal value // huffman literal value
buffer.put((byte)0x80); buffer.put((byte)0x80);
int needed = HuffmanEncoder.octetsNeeded(value); int needed = HuffmanEncoder.octetsNeeded(value);
if (needed >= 0)
{
NBitIntegerEncoder.encode(buffer, 7, needed); NBitIntegerEncoder.encode(buffer, 7, needed);
HuffmanEncoder.encode(buffer, value); HuffmanEncoder.encode(buffer, value);
} }
else else
{
// Not iso_8859_1
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeeded(bytes));
HuffmanEncoder.encode(buffer, bytes);
}
}
else
{ {
// add literal assuming iso_8859_1 // add literal assuming iso_8859_1
buffer.put((byte)0x00).mark(); buffer.put((byte)0x00).mark();
@ -479,15 +468,7 @@ public class HpackEncoder
for (int i = 0; i < value.length(); i++) for (int i = 0; i < value.length(); i++)
{ {
char c = value.charAt(i); char c = value.charAt(i);
if (c < ' ' || c > 127) c = Huffman.getReplacementCharacter(c);
{
// Not iso_8859_1, so re-encode as UTF-8
buffer.reset();
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NBitIntegerEncoder.encode(buffer, 7, bytes.length);
buffer.put(bytes, 0, bytes.length);
return;
}
buffer.put((byte)c); buffer.put((byte)c);
} }
} }

View File

@ -73,8 +73,8 @@ public class HpackFieldPreEncoder implements HttpFieldPreEncoder
else else
{ {
buffer.put((byte)0x80); buffer.put((byte)0x80);
NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLC(name)); NBitIntegerEncoder.encode(buffer, 7, HuffmanEncoder.octetsNeededLowercase(name));
HuffmanEncoder.encodeLC(buffer, name); HuffmanEncoder.encodeLowercase(buffer, name);
} }
HpackEncoder.encodeValue(buffer, huffman, value); HpackEncoder.encodeValue(buffer, huffman, value);

View File

@ -134,10 +134,9 @@ public class HpackTest
} }
@Test @Test
public void encodeDecodeNonAscii() throws Exception public void encodeNonAscii() throws Exception
{ {
HpackEncoder encoder = new HpackEncoder(); HpackEncoder encoder = new HpackEncoder();
HpackDecoder decoder = new HpackDecoder(4096, 8192);
ByteBuffer buffer = BufferUtil.allocate(16 * 1024); ByteBuffer buffer = BufferUtil.allocate(16 * 1024);
HttpFields fields0 = HttpFields.build() HttpFields fields0 = HttpFields.build()
@ -146,12 +145,14 @@ public class HpackTest
.add("custom-key", "[\uD842\uDF9F]"); .add("custom-key", "[\uD842\uDF9F]");
Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0); Response original0 = new MetaData.Response(HttpVersion.HTTP_2, 200, fields0);
HpackException.SessionException throwable = assertThrows(HpackException.SessionException.class, () ->
{
BufferUtil.clearToFill(buffer); BufferUtil.clearToFill(buffer);
encoder.encode(buffer, original0); encoder.encode(buffer, original0);
BufferUtil.flipToFlush(buffer, 0); BufferUtil.flipToFlush(buffer, 0);
Response decoded0 = (Response)decoder.decode(buffer); });
assertMetaDataSame(original0, decoded0); assertThat(throwable.getMessage(), containsString("Could not hpack encode"));
} }
@Test @Test

View File

@ -1,84 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.hpack;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.stream.Stream;
import org.eclipse.jetty.http.compression.HuffmanDecoder;
import org.eclipse.jetty.http.compression.HuffmanEncoder;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
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 = StringUtil.fromHexString(hex);
String decoded = HuffmanDecoder.decode(ByteBuffer.wrap(encoded), encoded.length);
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 = StringUtil.toHexString(BufferUtil.toArray(buf)).toLowerCase(Locale.ENGLISH);
assertEquals(hex, encoded, specSection);
assertEquals(hex.length() / 2, HuffmanEncoder.octetsNeeded(expected));
}
@ParameterizedTest(name = "[{index}]") // don't include unprintable character in test display-name
@ValueSource(chars = {(char)128, (char)0, (char)-1, ' ' - 1})
public void testEncode8859Only(char bad)
{
String s = "bad '" + bad + "'";
assertThat(HuffmanEncoder.octetsNeeded(s), Matchers.is(-1));
assertThrows(BufferOverflowException.class,
() -> HuffmanEncoder.encode(BufferUtil.allocate(32), s));
}
}

View File

@ -1,201 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.hpack;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.compression.NBitIntegerEncoder;
import org.eclipse.jetty.http.compression.NBitIntegerParser;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class NBitIntegerTest
{
@Test
public void testOctetsNeeded()
{
assertEquals(0, NBitIntegerEncoder.octetsNeeded(5, 10));
assertEquals(2, NBitIntegerEncoder.octetsNeeded(5, 1337));
assertEquals(1, NBitIntegerEncoder.octetsNeeded(8, 42));
assertEquals(3, NBitIntegerEncoder.octetsNeeded(8, 1337));
assertEquals(0, NBitIntegerEncoder.octetsNeeded(6, 62));
assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 63));
assertEquals(1, NBitIntegerEncoder.octetsNeeded(6, 64));
assertEquals(2, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x01));
assertEquals(3, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80));
assertEquals(4, NBitIntegerEncoder.octetsNeeded(6, 63 + 0x00 + 0x80 * 0x80 * 0x80));
}
@Test
public void testEncode()
{
testEncode(6, 0, "00");
testEncode(6, 1, "01");
testEncode(6, 62, "3e");
testEncode(6, 63, "3f00");
testEncode(6, 63 + 1, "3f01");
testEncode(6, 63 + 0x7e, "3f7e");
testEncode(6, 63 + 0x7f, "3f7f");
testEncode(6, 63 + 0x00 + 0x80 * 0x01, "3f8001");
testEncode(6, 63 + 0x01 + 0x80 * 0x01, "3f8101");
testEncode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01");
testEncode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002");
testEncode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102");
testEncode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f");
testEncode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001");
testEncode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f");
testEncode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001");
testEncode(8, 0, "00");
testEncode(8, 1, "01");
testEncode(8, 128, "80");
testEncode(8, 254, "Fe");
testEncode(8, 255, "Ff00");
testEncode(8, 255 + 1, "Ff01");
testEncode(8, 255 + 0x7e, "Ff7e");
testEncode(8, 255 + 0x7f, "Ff7f");
testEncode(8, 255 + 0x80, "Ff8001");
testEncode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001");
}
public void testEncode(int n, int i, String expected)
{
ByteBuffer buf = BufferUtil.allocate(16);
int p = BufferUtil.flipToFill(buf);
if (n < 8)
buf.put((byte)0x00);
NBitIntegerEncoder.encode(buf, n, i);
BufferUtil.flipToFlush(buf, p);
String r = StringUtil.toHexString(BufferUtil.toArray(buf));
assertEquals(expected, r);
assertEquals(expected.length() / 2, (n < 8 ? 1 : 0) + NBitIntegerEncoder.octetsNeeded(n, i));
}
@Test
public void testDecode() throws Exception
{
testDecode(6, 0, "00");
testDecode(6, 1, "01");
testDecode(6, 62, "3e");
testDecode(6, 63, "3f00");
testDecode(6, 63 + 1, "3f01");
testDecode(6, 63 + 0x7e, "3f7e");
testDecode(6, 63 + 0x7f, "3f7f");
testDecode(6, 63 + 0x80, "3f8001");
testDecode(6, 63 + 0x81, "3f8101");
testDecode(6, 63 + 0x7f + 0x80 * 0x01, "3fFf01");
testDecode(6, 63 + 0x00 + 0x80 * 0x02, "3f8002");
testDecode(6, 63 + 0x01 + 0x80 * 0x02, "3f8102");
testDecode(6, 63 + 0x7f + 0x80 * 0x7f, "3fFf7f");
testDecode(6, 63 + 0x00 + 0x80 * 0x80, "3f808001");
testDecode(6, 63 + 0x7f + 0x80 * 0x80 * 0x7f, "3fFf807f");
testDecode(6, 63 + 0x00 + 0x80 * 0x80 * 0x80, "3f80808001");
testDecode(8, 0, "00");
testDecode(8, 1, "01");
testDecode(8, 128, "80");
testDecode(8, 254, "Fe");
testDecode(8, 255, "Ff00");
testDecode(8, 255 + 1, "Ff01");
testDecode(8, 255 + 0x7e, "Ff7e");
testDecode(8, 255 + 0x7f, "Ff7f");
testDecode(8, 255 + 0x80, "Ff8001");
testDecode(8, 255 + 0x00 + 0x80 * 0x80, "Ff808001");
}
public void testDecode(int n, int expected, String encoded) throws Exception
{
ByteBuffer buf = ByteBuffer.wrap(StringUtil.fromHexString(encoded));
buf.position(n == 8 ? 0 : 1);
assertEquals(expected, NBitIntegerParser.decode(buf, n));
}
@Test
public void testEncodeExampleD11()
{
ByteBuffer buf = BufferUtil.allocate(16);
int p = BufferUtil.flipToFill(buf);
buf.put((byte)0x77);
buf.put((byte)0xFF);
NBitIntegerEncoder.encode(buf, 5, 10);
BufferUtil.flipToFlush(buf, p);
String r = StringUtil.toHexString(BufferUtil.toArray(buf));
assertEquals("77Ea", r);
}
@Test
public void testDecodeExampleD11() throws Exception
{
ByteBuffer buf = ByteBuffer.wrap(StringUtil.fromHexString("77EaFF"));
buf.position(2);
assertEquals(10, NBitIntegerParser.decode(buf, 5));
}
@Test
public void testEncodeExampleD12()
{
ByteBuffer buf = BufferUtil.allocate(16);
int p = BufferUtil.flipToFill(buf);
buf.put((byte)0x88);
buf.put((byte)0x00);
NBitIntegerEncoder.encode(buf, 5, 1337);
BufferUtil.flipToFlush(buf, p);
String r = StringUtil.toHexString(BufferUtil.toArray(buf));
assertEquals("881f9a0a", r);
}
@Test
public void testDecodeExampleD12() throws Exception
{
ByteBuffer buf = ByteBuffer.wrap(StringUtil.fromHexString("881f9a0aff"));
buf.position(2);
assertEquals(1337, NBitIntegerParser.decode(buf, 5));
}
@Test
public void testEncodeExampleD13()
{
ByteBuffer buf = BufferUtil.allocate(16);
int p = BufferUtil.flipToFill(buf);
buf.put((byte)0x88);
buf.put((byte)0xFF);
NBitIntegerEncoder.encode(buf, 8, 42);
BufferUtil.flipToFlush(buf, p);
String r = StringUtil.toHexString(BufferUtil.toArray(buf));
assertEquals("88Ff2a", r);
}
@Test
public void testDecodeExampleD13() throws Exception
{
ByteBuffer buf = ByteBuffer.wrap(StringUtil.fromHexString("882aFf"));
buf.position(1);
assertEquals(42, NBitIntegerParser.decode(buf, 8));
}
}

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http3.qpack.internal; package org.eclipse.jetty.http3.qpack.internal;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Objects; import java.util.Objects;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
@ -173,7 +174,7 @@ public abstract class EncodableEntry
{ {
buffer.put((byte)0x00); buffer.put((byte)0x00);
NBitIntegerEncoder.encode(buffer, 7, value.length()); NBitIntegerEncoder.encode(buffer, 7, value.length());
buffer.put(value.getBytes()); buffer.put(value.getBytes(StandardCharsets.ISO_8859_1));
} }
} }
@ -229,13 +230,12 @@ public abstract class EncodableEntry
} }
else else
{ {
// TODO: What charset should we be using? (this applies to the instruction generators as well).
buffer.put((byte)(0x20 | allowIntermediary)); buffer.put((byte)(0x20 | allowIntermediary));
NBitIntegerEncoder.encode(buffer, 3, name.length()); NBitIntegerEncoder.encode(buffer, 3, name.length());
buffer.put(name.getBytes()); buffer.put(name.getBytes(StandardCharsets.ISO_8859_1));
buffer.put((byte)0x00); buffer.put((byte)0x00);
NBitIntegerEncoder.encode(buffer, 7, value.length()); NBitIntegerEncoder.encode(buffer, 7, value.length());
buffer.put(value.getBytes()); buffer.put(value.getBytes(StandardCharsets.ISO_8859_1));
} }
} }
@ -268,7 +268,6 @@ public abstract class EncodableEntry
} }
} }
// TODO: pass in the HTTP version to avoid hard coding HTTP3?
private static class PreEncodedEntry extends EncodableEntry private static class PreEncodedEntry extends EncodableEntry
{ {
private final PreEncodedHttpField _httpField; private final PreEncodedHttpField _httpField;

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http3.qpack.internal.instruction; package org.eclipse.jetty.http3.qpack.internal.instruction;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.http.compression.HuffmanEncoder; import org.eclipse.jetty.http.compression.HuffmanEncoder;
import org.eclipse.jetty.http.compression.NBitIntegerEncoder; import org.eclipse.jetty.http.compression.NBitIntegerEncoder;
@ -72,7 +73,7 @@ public class IndexedNameEntryInstruction implements Instruction
{ {
buffer.put((byte)(0x00)); buffer.put((byte)(0x00));
NBitIntegerEncoder.encode(buffer, 7, _value.length()); NBitIntegerEncoder.encode(buffer, 7, _value.length());
buffer.put(_value.getBytes()); buffer.put(_value.getBytes(StandardCharsets.ISO_8859_1));
} }
BufferUtil.flipToFlush(buffer, 0); BufferUtil.flipToFlush(buffer, 0);

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.http3.qpack.internal.instruction; package org.eclipse.jetty.http3.qpack.internal.instruction;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.compression.HuffmanEncoder; import org.eclipse.jetty.http.compression.HuffmanEncoder;
@ -69,7 +70,7 @@ public class LiteralNameEntryInstruction implements Instruction
{ {
buffer.put((byte)(0x40)); buffer.put((byte)(0x40));
NBitIntegerEncoder.encode(buffer, 5, _name.length()); NBitIntegerEncoder.encode(buffer, 5, _name.length());
buffer.put(_name.getBytes()); buffer.put(_name.getBytes(StandardCharsets.ISO_8859_1));
} }
if (_huffmanValue) if (_huffmanValue)
@ -82,7 +83,7 @@ public class LiteralNameEntryInstruction implements Instruction
{ {
buffer.put((byte)(0x00)); buffer.put((byte)(0x00));
NBitIntegerEncoder.encode(buffer, 5, _value.length()); NBitIntegerEncoder.encode(buffer, 5, _value.length());
buffer.put(_value.getBytes()); buffer.put(_value.getBytes(StandardCharsets.ISO_8859_1));
} }
BufferUtil.flipToFlush(buffer, 0); BufferUtil.flipToFlush(buffer, 0);

View File

@ -0,0 +1,279 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* <p>Build a string from a sequence of bytes.</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
{
void append(byte b);
void append(char c);
default void append(byte[] bytes)
{
append(bytes, 0, bytes.length);
}
default void append(byte[] b, int offset, int length)
{
int end = offset + length;
for (int i = offset; i < end; i++)
append(b[i]);
}
default void append(CharSequence chars, int offset, int length)
{
int end = offset + length;
for (int i = offset; i < end; i++)
append(chars.charAt(i));
}
default void append(ByteBuffer buf)
{
int end = buf.position() + buf.remaining();
while (buf.position() < end)
append(buf.get());
}
/**
* <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();
static CharsetStringBuilder forCharset(Charset charset)
{
Objects.requireNonNull(charset);
if (charset == StandardCharsets.ISO_8859_1)
return new Iso8859StringBuilder();
if (charset == StandardCharsets.US_ASCII)
return new UsAsciiStringBuilder();
// Use a CharsetDecoder that defaults to CodingErrorAction#REPORT
return new DecoderStringBuilder(charset.newDecoder());
}
class Iso8859StringBuilder implements CharsetStringBuilder
{
private final StringBuilder _builder = new StringBuilder();
@Override
public void append(byte b)
{
_builder.append((char)(0xff & b));
}
@Override
public void append(char c)
{
_builder.append(c);
}
@Override
public void append(CharSequence chars, int offset, int length)
{
_builder.append(chars, offset, length);
}
@Override
public String build()
{
String s = _builder.toString();
_builder.setLength(0);
return s;
}
@Override
public void reset()
{
_builder.setLength(0);
}
}
class UsAsciiStringBuilder implements CharsetStringBuilder
{
private final StringBuilder _builder = new StringBuilder();
@Override
public void append(byte b)
{
if (b < 0)
throw new IllegalArgumentException();
_builder.append((char)b);
}
@Override
public void append(char c)
{
_builder.append(c);
}
@Override
public void append(CharSequence chars, int offset, int length)
{
_builder.append(chars, offset, length);
}
@Override
public String build()
{
String s = _builder.toString();
_builder.setLength(0);
return s;
}
@Override
public void reset()
{
_builder.setLength(0);
}
}
class DecoderStringBuilder implements CharsetStringBuilder
{
private final CharsetDecoder _decoder;
private final StringBuilder _stringBuilder = new StringBuilder(32);
private ByteBuffer _buffer = ByteBuffer.allocate(32);
public DecoderStringBuilder(CharsetDecoder charsetDecoder)
{
_decoder = charsetDecoder;
}
private void ensureSpace(int needed)
{
int space = _buffer.remaining();
if (space < needed)
{
int position = _buffer.position();
_buffer = ByteBuffer.wrap(Arrays.copyOf(_buffer.array(), _buffer.capacity() + needed - space + 32)).position(position);
}
}
@Override
public void append(byte b)
{
ensureSpace(1);
_buffer.put(b);
}
@Override
public void append(char c)
{
if (_buffer.position() > 0)
{
try
{
// Append any data already in the decoder
_stringBuilder.append(_decoder.decode(_buffer.flip()));
_buffer.clear();
}
catch (CharacterCodingException e)
{
// This will be thrown only if the decoder is configured to REPORT,
// otherwise errors will be ignored or replaced and we will not catch here.
throw new RuntimeException(e);
}
}
_stringBuilder.append(c);
}
@Override
public void append(CharSequence chars, int offset, int length)
{
if (_buffer.position() > 0)
{
try
{
// Append any data already in the decoder
_stringBuilder.append(_decoder.decode(_buffer.flip()));
_buffer.clear();
}
catch (CharacterCodingException e)
{
// This will be thrown only if the decoder is configured to REPORT,
// otherwise errors will be ignored or replaced and we will not catch here.
throw new RuntimeException(e);
}
}
_stringBuilder.append(chars, offset, offset + length);
}
@Override
public void append(byte[] b, int offset, int length)
{
ensureSpace(length);
_buffer.put(b, offset, length);
}
@Override
public void append(ByteBuffer buf)
{
ensureSpace(buf.remaining());
_buffer.put(buf);
}
@Override
public String build() throws CharacterCodingException
{
try
{
if (_buffer.position() > 0)
{
CharSequence decoded = _decoder.decode(_buffer.flip());
_buffer.clear();
if (_stringBuilder.length() == 0)
return decoded.toString();
_stringBuilder.append(decoded);
}
return _stringBuilder.toString();
}
finally
{
_stringBuilder.setLength(0);
}
}
@Override
public void reset()
{
_stringBuilder.setLength(0);
}
}
}