Skip to content

Commit e126104

Browse files
committed
Let hex decoder accept even and odd number of string length
With auto zero padding (ie. 'E3F' is same as '0E3F'). Also adds more tests for error cases and improve javadoc refs #37
1 parent f20f00b commit e126104

File tree

4 files changed

+55
-9
lines changed

4 files changed

+55
-9
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## v1.2.0
44

5+
* let hex decoder accept odd length string #37
6+
57
## v1.1.0
68

79
* add `unsecureRandom()` constructor which creates random for e.g. tests or deterministic randoms

src/main/java/at/favre/lib/bytes/BinaryToTextEncoding.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,27 +104,39 @@ public String encode(byte[] byteArray, ByteOrder byteOrder) {
104104

105105
@Override
106106
public byte[] decode(CharSequence hexString) {
107-
if (Objects.requireNonNull(hexString, "hex input must not be null").length() % 2 != 0)
108-
throw new IllegalArgumentException("invalid hex string, must be mod 2 == 0");
109107

110108
int start;
111-
if (hexString.length() > 2 &&
109+
if (Objects.requireNonNull(hexString).length() > 2 &&
112110
hexString.charAt(0) == '0' && hexString.charAt(1) == 'x') {
113111
start = 2;
114112
} else {
115113
start = 0;
116114
}
117115

116+
118117
int len = hexString.length();
118+
boolean isOddLength = len % 2 != 0;
119+
if (isOddLength) {
120+
start--;
121+
}
122+
119123
byte[] data = new byte[(len - start) / 2];
120124
int first4Bits;
121125
int second4Bits;
122126
for (int i = start; i < len; i += 2) {
123-
first4Bits = Character.digit(hexString.charAt(i), 16);
127+
if (i == start && isOddLength) {
128+
first4Bits = 0;
129+
} else {
130+
first4Bits = Character.digit(hexString.charAt(i), 16);
131+
}
124132
second4Bits = Character.digit(hexString.charAt(i + 1), 16);
125133

126134
if (first4Bits == -1 || second4Bits == -1) {
127-
throw new IllegalArgumentException("'" + hexString.charAt(i) + hexString.charAt(i + 1) + "' at index " + i + " is not hex formatted");
135+
if (i == start && isOddLength) {
136+
throw new IllegalArgumentException("'" + hexString.charAt(i + 1) + "' at index " + (i + 1) + " is not hex formatted");
137+
} else {
138+
throw new IllegalArgumentException("'" + hexString.charAt(i) + hexString.charAt(i + 1) + "' at index " + i + " is not hex formatted");
139+
}
128140
}
129141

130142
data[(i - start) / 2] = (byte) ((first4Bits << 4) + second4Bits);

src/main/java/at/favre/lib/bytes/Bytes.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,11 +593,17 @@ public static Bytes parseRadix(CharSequence radixNumberString, int radix) {
593593
}
594594

595595
/**
596-
* Parsing of base16/HEX encoded byte arrays. Will accept upper- and lowercase variant and ignores
597-
* possible "0x" prefix.
596+
* Parsing of base16/HEX encoded byte arrays. This is by design a very flexible decoder accepting the following cases:
597+
*
598+
* <ul>
599+
* <li>Upper- and lowercase <code>a-f</code> (also mixed case)</li>
600+
* <li>Prefix with <code>0x</code> which will be ignored</li>
601+
* <li>Even and odd number of string length with auto zero padding (ie. 'E3F' is same as '0E3F')</li>
602+
* </ul>
598603
*
599604
* @param hexString the encoded string
600605
* @return decoded instance
606+
* @throws IllegalArgumentException if string contains something else than [0-9a-fA-F]
601607
*/
602608
public static Bytes parseHex(CharSequence hexString) {
603609
return parse(hexString, new BinaryToTextEncoding.Hex());

src/test/java/at/favre/lib/bytes/BytesParseAndEncodingTest.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,37 @@ public void parseHex() {
4242
assertArrayEquals(defaultArray, Bytes.parseHex("A0E1").array());
4343
assertArrayEquals(defaultArray, Bytes.parseHex("a0e1").array());
4444
assertArrayEquals(defaultArray, Bytes.parseHex(Bytes.parseHex("A0E1").encodeHex()).array());
45+
assertArrayEquals(defaultArray, Bytes.parseHex(Bytes.parseHex("a0E1").encodeHex()).array());
46+
}
47+
48+
@Test
49+
public void parseHexOddNumberStrings() {
50+
assertArrayEquals(new byte[]{(byte) 0x00}, Bytes.parseHex("0").array());
51+
assertArrayEquals(new byte[]{(byte) 0x0E}, Bytes.parseHex("E").array());
52+
assertArrayEquals(new byte[]{(byte) 0x0A, (byte) 0x0E}, Bytes.parseHex("A0E").array());
53+
assertArrayEquals(new byte[]{(byte) 0x03, (byte) 0xEA, (byte) 0x0E}, Bytes.parseHex("3EA0E").array());
54+
assertArrayEquals(new byte[]{(byte) 0x00, (byte) 0xF3, (byte) 0xEA, (byte) 0x0E}, Bytes.parseHex("0F3EA0E").array());
55+
assertArrayEquals(new byte[]{(byte) 0x0A, (byte) 0xD0, (byte) 0xF3, (byte) 0xEA, (byte) 0x0E}, Bytes.parseHex("AD0F3EA0E").array());
56+
}
57+
58+
@Test(expected = IllegalArgumentException.class)
59+
public void parseHexIllegalChars() {
60+
Bytes.parseHex("AX");
61+
}
62+
63+
@Test(expected = IllegalArgumentException.class)
64+
public void parseHexIllegalChars2() {
65+
Bytes.parseHex("XX");
66+
}
67+
68+
@Test(expected = IllegalArgumentException.class)
69+
public void parseHexIllegalChars3() {
70+
Bytes.parseHex("Y");
4571
}
4672

4773
@Test(expected = IllegalArgumentException.class)
48-
public void parseHexInvalid() {
49-
Bytes.parseHex("A0E");
74+
public void parseHexIllegalChars4() {
75+
Bytes.parseHex("F3El0E");
5076
}
5177

5278
@Test

0 commit comments

Comments
 (0)