diff options
5 files changed, 179 insertions, 97 deletions
diff --git a/luni/src/main/java/libcore/util/HexEncoding.java b/luni/src/main/java/libcore/util/HexEncoding.java new file mode 100644 index 0000000..f883a73 --- /dev/null +++ b/luni/src/main/java/libcore/util/HexEncoding.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +/** + * Hexadecimal encoding where each byte is represented by two hexadecimal digits. + */ +public class HexEncoding { + + /** Hidden constructor to prevent instantiation. */ + private HexEncoding() {} + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + + /** + * Encodes the provided data as a sequence of hexadecimal characters. + */ + public static char[] encode(byte[] data) { + return encode(data, 0, data.length); + } + + /** + * Encodes the provided data as a sequence of hexadecimal characters. + */ + public static char[] encode(byte[] data, int offset, int len) { + char[] result = new char[len * 2]; + for (int i = 0; i < len; i++) { + byte b = data[offset + i]; + int resultIndex = 2 * i; + result[resultIndex] = (HEX_DIGITS[(b >>> 4) & 0x0f]); + result[resultIndex + 1] = (HEX_DIGITS[b & 0x0f]); + } + + return result; + } + + /** + * Decodes the provided hexadecimal string into a byte array. If {@code allowSingleChar} + * is {@code true} odd-length inputs are allowed and the first character is interpreted + * as the lower bits of the first result byte. + * + * Throws an {@code IllegalArgumentException} if the input is malformed. + */ + public static byte[] decode(char[] encoded, boolean allowSingleChar) throws IllegalArgumentException { + int resultLengthBytes = (encoded.length + 1) / 2; + byte[] result = new byte[resultLengthBytes]; + + int resultOffset = 0; + int i = 0; + if (allowSingleChar) { + if ((encoded.length % 2) != 0) { + // Odd number of digits -- the first digit is the lower 4 bits of the first result byte. + result[resultOffset++] = (byte) toDigit(encoded, i); + i++; + } + } else { + if ((encoded.length % 2) != 0) { + throw new IllegalArgumentException("Invalid input length: " + encoded.length); + } + } + + for (int len = encoded.length; i < len; i += 2) { + result[resultOffset++] = (byte) ((toDigit(encoded, i) << 4) | toDigit(encoded, i + 1)); + } + + return result; + } + + private static int toDigit(char[] str, int offset) throws IllegalArgumentException { + // NOTE: that this isn't really a code point in the traditional sense, since we're + // just rejecting surrogate pairs outright. + int pseudoCodePoint = str[offset]; + + if ('0' <= pseudoCodePoint && pseudoCodePoint <= '9') { + return pseudoCodePoint - '0'; + } else if ('a' <= pseudoCodePoint && pseudoCodePoint <= 'f') { + return 10 + (pseudoCodePoint - 'a'); + } else if ('A' <= pseudoCodePoint && pseudoCodePoint <= 'F') { + return 10 + (pseudoCodePoint - 'A'); + } + + throw new IllegalArgumentException("Illegal char: " + str[offset] + + " at offset " + offset); + } +} diff --git a/luni/src/test/java/libcore/util/HexEncodingTest.java b/luni/src/test/java/libcore/util/HexEncodingTest.java new file mode 100644 index 0000000..ef79f5c --- /dev/null +++ b/luni/src/test/java/libcore/util/HexEncodingTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import junit.framework.TestCase; +import static libcore.util.HexEncoding.decode; +import static libcore.util.HexEncoding.encode; + +public class HexEncodingTest extends TestCase { + public void testEncode() { + final byte[] avocados = "avocados".getBytes(StandardCharsets.UTF_8); + + assertArraysEqual("61766f6361646f73".toCharArray(), encode(avocados)); + assertArraysEqual(avocados, decode(encode(avocados), false)); + } + + public void testDecode_allow4Bit() { + assertArraysEqual(new byte[] { 6 }, decode("6".toCharArray(), true)); + assertArraysEqual(new byte[] { 6, 'v' }, decode("676".toCharArray(), true)); + } + + public void testDecode_disallow4Bit() { + try { + decode("676".toCharArray(), false); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testDecode_invalid() { + try { + decode("deadbard".toCharArray(), false); + fail(); + } catch (IllegalArgumentException expected) { + } + + // This demonstrates a difference in behaviour from apache commons : apache + // commons uses Character.isDigit and would successfully decode a string with + // arabic and devanagari characters. + try { + decode("६१٧٥٥f6361646f73".toCharArray(), false); + fail(); + } catch (IllegalArgumentException expected) { + } + + try { + decode("#%6361646f73".toCharArray(), false); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + private static void assertArraysEqual(char[] lhs, char[] rhs) { + assertEquals(new String(lhs), new String(rhs)); + } + + private static void assertArraysEqual(byte[] lhs, byte[] rhs) { + assertEquals(Arrays.toString(lhs), Arrays.toString(rhs)); + } +} diff --git a/support/src/test/java/libcore/tlswire/handshake/ClientHello.java b/support/src/test/java/libcore/tlswire/handshake/ClientHello.java index 8d25cd5..ec88662 100644 --- a/support/src/test/java/libcore/tlswire/handshake/ClientHello.java +++ b/support/src/test/java/libcore/tlswire/handshake/ClientHello.java @@ -17,8 +17,8 @@ package libcore.tlswire.handshake; import libcore.tlswire.util.TlsProtocolVersion; -import libcore.tlswire.util.HexEncoding; import libcore.tlswire.util.IoUtils; +import libcore.util.HexEncoding; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; @@ -98,8 +98,8 @@ public class ClientHello extends HandshakeMessage { @Override public String toString() { return "ClientHello{client version: " + clientVersion - + ", random: " + HexEncoding.encode(random) - + ", sessionId: " + HexEncoding.encode(sessionId) + + ", random: " + new String(HexEncoding.encode(random)) + + ", sessionId: " + new String(HexEncoding.encode(sessionId)) + ", cipher suites: " + cipherSuites + ", compression methods: " + compressionMethods + ((extensions != null) ? (", extensions: " + String.valueOf(extensions)) : "") diff --git a/support/src/test/java/libcore/tlswire/handshake/HelloExtension.java b/support/src/test/java/libcore/tlswire/handshake/HelloExtension.java index e3361b9..5741072 100644 --- a/support/src/test/java/libcore/tlswire/handshake/HelloExtension.java +++ b/support/src/test/java/libcore/tlswire/handshake/HelloExtension.java @@ -16,8 +16,8 @@ package libcore.tlswire.handshake; -import libcore.tlswire.util.HexEncoding; import libcore.tlswire.util.IoUtils; +import libcore.util.HexEncoding; import java.io.DataInput; import java.io.IOException; import java.util.HashMap; diff --git a/support/src/test/java/libcore/tlswire/util/HexEncoding.java b/support/src/test/java/libcore/tlswire/util/HexEncoding.java deleted file mode 100644 index 2061fcc..0000000 --- a/support/src/test/java/libcore/tlswire/util/HexEncoding.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package libcore.tlswire.util; - -import java.nio.ByteBuffer; - -/** - * Hexadecimal encoding where each byte is represented by two hexadecimal digits. - */ -public class HexEncoding { - - /** Hidden constructor to prevent instantiation. */ - private HexEncoding() {} - - private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); - - /** - * Encodes the provided data as a hexadecimal string. - */ - public static String encode(byte[] data) { - return encode(data, 0, data.length); - } - - /** - * Encodes the provided data as a hexadecimal string. - */ - public static String encode(byte[] data, int offset, int len) { - StringBuilder result = new StringBuilder(len * 2); - for (int i = 0; i < len; i++) { - byte b = data[offset + i]; - result.append(HEX_DIGITS[(b >>> 4) & 0x0f]); - result.append(HEX_DIGITS[b & 0x0f]); - } - return result.toString(); - } - - /** - * Encodes the provided data as a hexadecimal string. - */ - public static String encode(ByteBuffer buf) { - return encode(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); - } - - /** - * Decodes the provided hexadecimal string into an array of bytes. - */ - public static byte[] decode(String encoded) { - // IMPLEMENTATION NOTE: Special care is taken to permit odd number of hexadecimal digits. - int resultLengthBytes = (encoded.length() + 1) / 2; - byte[] result = new byte[resultLengthBytes]; - int resultOffset = 0; - int encodedCharOffset = 0; - if ((encoded.length() % 2) != 0) { - // Odd number of digits -- the first digit is the lower 4 bits of the first result byte. - result[resultOffset++] = - (byte) getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)); - encodedCharOffset++; - } - for (int len = encoded.length(); encodedCharOffset < len; encodedCharOffset += 2) { - result[resultOffset++] = (byte) ( - (getHexadecimalDigitValue(encoded.charAt(encodedCharOffset)) << 4) - | getHexadecimalDigitValue(encoded.charAt(encodedCharOffset + 1))); - } - return result; - } - - private static int getHexadecimalDigitValue(char c) { - if ((c >= 'a') && (c <= 'f')) { - return (c - 'a') + 0x0a; - } else if ((c >= 'A') && (c <= 'F')) { - return (c - 'A') + 0x0a; - } else if ((c >= '0') && (c <= '9')) { - return c - '0'; - } else { - throw new IllegalArgumentException("Invalid hexadecimal digit at position : '" + c - + "' (0x" + Integer.toHexString(c) + ")"); - } - } -} |