diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java
index 8aebd005f8b3..720faf4dd075 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpTokens.java
@@ -188,5 +188,49 @@ else if (b >= 0x80) // OBS
}
}
}
+
+ /**
+ * 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.
+ *
+ * field-vchar = VCHAR / obs-text
+ * obs-text = %x80-FF
+ * VCHAR = %x21-7E
+ *
+ * @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 < ' ');
+ }
}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java
new file mode 100644
index 000000000000..5f0555765367
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/EncodingException.java
@@ -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);
+ }
+}
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java
similarity index 82%
rename from jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java
rename to jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java
index 26e948b6644e..6e2cb33c21e6 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/Huffman.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/Huffman.java
@@ -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));
- }
- }
}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java
new file mode 100644
index 000000000000..48851c113f11
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanDecoder.java
@@ -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;
+
+/**
+ * Used to decoded Huffman encoded strings.
+ *
+ * Characters which are illegal field-vchar values are replaced with
+ * either ' ' or '?' as described in RFC9110
+ */
+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;
+ }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java
new file mode 100644
index 000000000000..533fc21dca8b
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/HuffmanEncoder.java
@@ -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;
+
+/**
+ * Used to encode strings Huffman encoding.
+ *
+ * Characters are encoded with ISO-8859-1, if any multi-byte characters or
+ * control characters are present the encoder will throw {@link EncodingException}.
+ */
+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));
+ }
+ }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java
new file mode 100644
index 000000000000..590b8c195a22
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerDecoder.java
@@ -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;
+ }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java
new file mode 100644
index 000000000000..f78440f5f2ae
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitIntegerEncoder.java
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java
new file mode 100644
index 000000000000..a8715976739a
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringDecoder.java
@@ -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;
+
+/**
+ * Used to decode string literals as described in RFC7541.
+ *
+ * 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.
+ *
+ * Characters which are illegal field-vchar values are replaced with
+ * either ' ' or '?' as described in RFC9110
+ */
+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;
+ }
+}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java
new file mode 100644
index 000000000000..5729ec068314
--- /dev/null
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/compression/NBitStringEncoder.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java
new file mode 100644
index 000000000000..e9e5ae86ecfe
--- /dev/null
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HuffmanTest.java
@@ -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 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 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 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());
+ }
+}
diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java
similarity index 77%
rename from jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java
rename to jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java
index 84353117c461..03f7b5f2172b 100644
--- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/NBitIntegerTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/NBitIntegerTest.java
@@ -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(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(5, 10));
+ assertEquals(3, NBitIntegerEncoder.octetsNeeded(5, 1337));
+ assertEquals(1, NBitIntegerEncoder.octetsNeeded(8, 42));
+ assertEquals(3, NBitIntegerEncoder.octetsNeeded(8, 1337));
+
+ 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 void testEncode(int n, int i, String expected)
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 void testDecode()
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 void testEncodeExampleD11()
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 void testEncodeExampleD11()
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 void testEncodeExampleD12()
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 void testEncodeExampleD12()
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 void testEncodeExampleD13()
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 void testDecodeExampleD13()
{
ByteBuffer buf = ByteBuffer.wrap(TypeUtil.fromHexString("882aFf"));
buf.position(1);
-
- assertEquals(42, NBitInteger.decode(buf, 8));
+ _decoder.setPrefix(8);
+ assertEquals(42, _decoder.decodeInt(buf));
}
}
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java
index ea6b58458a8f..c3bd42bc2891 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackContext.java
@@ -29,6 +29,8 @@
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 static class StaticEntry extends Entry
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;
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java
index 493b4f425d72..d888d5a40194 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackDecoder.java
@@ -24,8 +24,12 @@
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 HpackDecoder(int localMaxDynamicTableSize, int maxHeaderSize)
_context = new HpackContext(localMaxDynamicTableSize);
_localMaxDynamicTableSize = localMaxDynamicTableSize;
_builder = new MetaDataBuilder(maxHeaderSize);
+ _huffmanDecoder = new HuffmanDecoder();
+ _integerDecoder = new NBitIntegerDecoder();
}
public HpackContext getHpackContext()
@@ -69,27 +77,22 @@ public MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
// 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 MetaData decode(ByteBuffer buffer) throws HpackException.SessionException
return _builder.build();
}
- public static String toASCIIString(ByteBuffer buffer, int length)
+ private int integerDecode(ByteBuffer buffer, int prefix) throws HpackException.CompressionException
+ {
+ try
+ {
+ 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;
+ }
+ 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)
{
- 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++)
+ CharsetStringBuilder.Iso88591StringBuilder builder = new CharsetStringBuilder.Iso88591StringBuilder();
+ for (int i = 0; i < length; ++i)
{
- builder.append((char)(0x7f & array[i]));
+ builder.append(HttpTokens.sanitizeFieldVchar((char)buffer.get()));
}
- return builder.toString();
+ return builder.build();
}
@Override
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java
index 101125207e25..18774e7b8fe7 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackEncoder.java
@@ -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.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 @@ else if (metadata.isResponse())
}
}
- // 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 @@ else if (metadata.isResponse())
}
}
- 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 void encode(ByteBuffer buffer, HttpField field)
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 void encode(ByteBuffer buffer, HttpField field)
{
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 @@ else if (DO_NOT_INDEX.contains(header))
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 @@ else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.
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 @@ else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.
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 @@ private void encodeName(ByteBuffer buffer, byte mask, int bits, String name, Ent
// 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);
}
}
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java
index 2aad1221d2f5..df89d802c693 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackException.java
@@ -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 static class StreamException extends HpackException
*/
public static class SessionException extends HpackException
{
- SessionException(String messageFormat, Object... args)
+ public SessionException(String messageFormat, Object... args)
{
super(messageFormat, args);
}
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java
index 769819dff3fb..629ef2c5fe82 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/HpackFieldPreEncoder.java
@@ -23,6 +23,8 @@
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 @@
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 @@ else if (header == HttpHeader.CONTENT_LENGTH && value.length() > 1)
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);
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java
index df45309f011c..9ed7253f52d2 100644
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java
+++ b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/MetaDataBuilder.java
@@ -29,7 +29,7 @@
public class MetaDataBuilder
{
- private final int _maxSize;
+ private int _maxSize;
private int _size;
private Integer _status;
private String _method;
@@ -60,6 +60,11 @@ public int getMaxSize()
return _maxSize;
}
+ public void setMaxSize(int maxSize)
+ {
+ _maxSize = maxSize;
+ }
+
/**
* Get the size.
*
@@ -70,17 +75,18 @@ public int getSize()
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 void emit(HttpField field) throws HpackException.SessionException
{
case C_STATUS:
if (checkPseudoHeader(header, _status))
- _status = (Integer)staticField.getStaticValue();
+ _status = staticField.getIntValue();
_response = true;
break;
@@ -157,7 +163,7 @@ else if (value != null)
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 @@ else if (value != null)
}
}
- 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 MetaData build() throws HpackException.StreamException
_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);
- }
}
diff --git a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java b/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java
deleted file mode 100644
index bab61bbd4ae9..000000000000
--- a/jetty-http2/http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/NBitInteger.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java
index ed0d914c8e2a..a767a51fd2fd 100644
--- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java
+++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackContextTest.java
@@ -21,6 +21,9 @@
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 @@
*/
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 void testStaticHuffmanValues() throws Exception
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);
}
diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java
index 4cc50c593177..b75c713995f7 100644
--- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java
+++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackDecoderTest.java
@@ -470,7 +470,7 @@ public void testHuffmanEncodedExtraPadding()
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 void testHuffmanEncodedZeroPadding()
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 void testHuffmanEncodedWithEOS()
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 void testHuffmanEncodedOneIncompleteOctet()
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 void testHuffmanEncodedTwoIncompleteOctet()
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
diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java
index 72155e51aff3..5a20d348bebf 100644
--- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java
+++ b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HpackTest.java
@@ -133,7 +133,7 @@ public void encodeDecodeTooLargeTest() throws Exception
}
catch (HpackException.SessionException e)
{
- assertThat(e.getMessage(), containsString("Header too large"));
+ assertThat(e.getMessage(), containsString("Header size 198 > 164"));
}
}
@@ -141,21 +141,22 @@ public void encodeDecodeTooLargeTest() throws Exception
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
diff --git a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java b/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java
deleted file mode 100644
index 1b62829e4eb4..000000000000
--- a/jetty-http2/http2-hpack/src/test/java/org/eclipse/jetty/http2/hpack/HuffmanTest.java
+++ /dev/null
@@ -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 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));
- }
-}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java
new file mode 100644
index 000000000000..13f588e8aa22
--- /dev/null
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/CharsetStringBuilder.java
@@ -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;
+
+/**
+ * Build a string from a sequence of bytes and/or characters.
+ * 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.
+ * Any coding errors in the string will be reported by a {@link CharacterCodingException} thrown
+ * from the {@link #build()} method.
+ * @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());
+ }
+
+ /**
+ * Build the completed string and reset the buffer.
+ * @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);
+ }
+ }
+}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java
index 0bdb229eeec4..b0535801e50c 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java
@@ -107,6 +107,27 @@ public static String normalizeCharset(String s, int offset, int length)
'\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 static String asciiToLowerCase(String s)
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.