Move QueryStringDecoder.decodeHexByte into ByteBufUtil

Motivations:
1. There are duplicated implementations of decoding hex strings. #6797
2. ByteBufUtil.HexUtil.decodeHexDump does not handle substring start
index properly and does not decode hex byte rigorously.

Modifications:
1. Function decodeHexByte is moved from QueryStringDecoder into ByteBufUtil.
2. ByteBufUtil.HexUtil.decodeHexDump is changed to use decodeHexByte.
3. Tests are Updated accordingly.

Result:
Fixed #6797 and made hex decoding functions more robust.
This commit is contained in:
Renjie Sun 2017-06-03 08:27:44 +08:00 committed by Scott Mitchell
parent ed5fcbb773
commit 629b83e0a5
4 changed files with 77 additions and 48 deletions

View File

@ -125,6 +125,13 @@ public final class ByteBufUtil {
return HexUtil.hexDump(array, fromIndex, length);
}
/**
* Decode a 2-digit hex byte from within a string.
*/
public static byte decodeHexByte(CharSequence s, int pos) {
return HexUtil.decodeHexByte(s, pos);
}
/**
* Decodes a string generated by {@link #hexDump(byte[])}
*/
@ -132,6 +139,13 @@ public final class ByteBufUtil {
return HexUtil.decodeHexDump(hexDump, 0, hexDump.length());
}
/**
* Decodes part of a string generated by {@link #hexDump(byte[])}
*/
public static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
return HexUtil.decodeHexDump(hexDump, fromIndex, length);
}
/**
* Calculates the hash code of the specified buffer. This method is
* useful when implementing a new buffer type.
@ -877,20 +891,50 @@ public final class ByteBufUtil {
return new String(buf);
}
/**
* Helper to decode half of a hexadecimal number from a string.
* @param c The ASCII character of the hexadecimal number to decode.
* Must be in the range {@code [0-9a-fA-F]}.
* @return The hexadecimal value represented in the ASCII character
* given, or {@code -1} if the character is invalid.
*/
private static int decodeHexNibble(final char c) {
// Character.digit() is not used here, as it addresses a larger
// set of characters (both ASCII and full-width latin letters).
if ('0' <= c && c <= '9') {
return c - '0';
}
if ('a' <= c && c <= 'f') {
return c - 'a' + 0xA;
}
if ('A' <= c && c <= 'F') {
return c - 'A' + 0xA;
}
return -1;
}
private static byte decodeHexByte(CharSequence s, int pos) {
int hi = decodeHexNibble(s.charAt(pos));
int lo = decodeHexNibble(s.charAt(pos + 1));
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException(String.format(
"invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
}
return (byte) ((hi << 4) + lo);
}
private static byte[] decodeHexDump(CharSequence hexDump, int fromIndex, int length) {
if (length < 0) {
if (length < 0 || (length & 1) != 0) {
throw new IllegalArgumentException("length: " + length);
}
if (length == 0) {
return EmptyArrays.EMPTY_BYTES;
}
int endIndex = fromIndex + length - 1;
byte[] bytes = new byte[length >>> 1];
for (; fromIndex < endIndex; fromIndex += 2) {
bytes[fromIndex >>> 1] = (byte) ((Character.digit(hexDump.charAt(fromIndex), 16) << 4) +
Character.digit(hexDump.charAt(fromIndex + 1), 16));
for (int i = 0; i < length; i += 2) {
bytes[i >>> 1] = decodeHexByte(hexDump, fromIndex + i);
}
return bytes;

View File

@ -20,6 +20,7 @@ import io.netty.util.CharsetUtil;
import org.junit.Test;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;
import static io.netty.buffer.Unpooled.unreleasableBuffer;
@ -31,22 +32,35 @@ import static org.junit.Assert.fail;
public class ByteBufUtilTest {
@Test
public void decodeHexEvenLength() {
decodeHex(256);
public void decodeRandomHexBytesWithEvenLength() {
decodeRandomHexBytes(256);
}
@Test
public void decodeHexOddLength() {
decodeHex(257);
public void decodeRandomHexBytesWithOddLength() {
decodeRandomHexBytes(257);
}
private static void decodeHex(int len) {
private static void decodeRandomHexBytes(int len) {
byte[] b = new byte[len];
Random rand = new Random();
rand.nextBytes(b);
String hexDump = ByteBufUtil.hexDump(b);
byte[] decodedBytes = ByteBufUtil.decodeHexDump(hexDump);
assertArrayEquals(b, decodedBytes);
for (int i = 0; i <= len; i++) { // going over sub-strings of various lengths including empty byte[].
byte[] b2 = Arrays.copyOfRange(b, i, b.length);
byte[] decodedBytes = ByteBufUtil.decodeHexDump(hexDump, i * 2, (len - i) * 2);
assertArrayEquals(b2, decodedBytes);
}
}
@Test(expected = IllegalArgumentException.class)
public void decodeHexDumpWithOddLength() {
ByteBufUtil.decodeHexDump("abc");
}
@Test(expected = IllegalArgumentException.class)
public void decodeHexDumpWithInvalidChar() {
ByteBufUtil.decodeHexDump("fg");
}
@Test

View File

@ -33,6 +33,7 @@ import java.util.Map;
import static io.netty.util.internal.ObjectUtil.*;
import static io.netty.util.internal.StringUtil.*;
import static io.netty.buffer.ByteBufUtil.decodeHexByte;
/**
* Splits an HTTP query string into a path string and key-value parameter pairs.
@ -330,7 +331,10 @@ public class QueryStringDecoder {
byteBuf.clear();
do {
byteBuf.put(decodeHexByte(s, i, toExcluded));
if (i + 3 > toExcluded) {
throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
}
byteBuf.put(decodeHexByte(s, i + 1));
i += 3;
} while (i < toExcluded && s.charAt(i) == '%');
i--;
@ -354,39 +358,6 @@ public class QueryStringDecoder {
return strBuf.toString();
}
private static byte decodeHexByte(String s, int pos, int len) {
if (pos + 2 >= len) {
throw new IllegalArgumentException("unterminated escape sequence at index " + pos + " of: " + s);
}
int hi = decodeHexNibble(s.charAt(pos + 1));
int lo = decodeHexNibble(s.charAt(pos + 2));
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException(
"invalid escape sequence '" + s.substring(pos, pos + 3) + "' at index " + pos + " of: " + s);
}
return (byte) ((hi << 4) + lo);
}
/**
* Helper to decode half of a hexadecimal number from a string.
* @param c The ASCII character of the hexadecimal number to decode.
* Must be in the range {@code [0-9a-fA-F]}.
* @return The hexadecimal value represented in the ASCII character
* given, or {@code -1} if the character is invalid.
*/
private static int decodeHexNibble(final char c) {
if ('0' <= c && c <= '9') {
return c - '0';
}
if ('a' <= c && c <= 'f') {
return c - 'a' + 0xA;
}
if ('A' <= c && c <= 'F') {
return c - 'A' + 0xA;
}
return -1;
}
private static int findPathEndIndex(String uri) {
int len = uri.length();
for (int i = 0; i < len; i++) {

View File

@ -205,8 +205,8 @@ public class QueryStringDecoderTest {
"%42", "B",
"%5f", "_",
"f%4", "unterminated escape sequence at index 1 of: f%4",
"%x2", "invalid escape sequence '%x2' at index 0 of: %x2",
"%4x", "invalid escape sequence '%4x' at index 0 of: %4x",
"%x2", "invalid hex byte 'x2' at index 1 of '%x2'",
"%4x", "invalid hex byte '4x' at index 1 of '%4x'",
"Caff%C3%A9", caffe,
"случайный праздник", "случайный праздник",
"случайный%20праздник", "случайный праздник",