This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-codec.git
commit 081756b86e25ec5ea7bc786e88a7d56b76f88ef9 Author: Adam Retter <adam.ret...@googlemail.com> AuthorDate: Sat Apr 18 23:56:17 2020 +0200 Add Base16 Input and Output Streams --- pom.xml | 7 + .../org/apache/commons/codec/binary/Base16.java | 268 +++++++++ .../commons/codec/binary/Base16InputStream.java | 67 +++ .../commons/codec/binary/Base16OutputStream.java | 67 +++ .../java/org/apache/commons/codec/binary/Hex.java | 85 ++- .../codec/binary/Base16InputStreamTest.java | 411 ++++++++++++++ .../codec/binary/Base16OutputStreamTest.java | 266 +++++++++ .../apache/commons/codec/binary/Base16Test.java | 620 +++++++++++++++++++++ .../commons/codec/binary/Base16TestData.java | 111 ++++ .../org/apache/commons/codec/binary/HexTest.java | 34 ++ 10 files changed, 1929 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 05bb8d3..8041b03 100644 --- a/pom.xml +++ b/pom.xml @@ -207,6 +207,13 @@ limitations under the License. <role>Submitted Match Rating Approach (MRA) phonetic encoder and tests [CODEC-161]</role> </roles> </contributor> + <contributor> + <name>Adam Retter</name> + <organization>Evolved Binary</organization> + <roles> + <role>Base16 Input and Output Streams</role> + </roles> + </contributor> </contributors> <!-- Codec only has test dependencies ATM --> <dependencies> diff --git a/src/main/java/org/apache/commons/codec/binary/Base16.java b/src/main/java/org/apache/commons/codec/binary/Base16.java new file mode 100644 index 0000000..f94c115 --- /dev/null +++ b/src/main/java/org/apache/commons/codec/binary/Base16.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import org.apache.commons.codec.DecoderException; + +import java.nio.charset.Charset; + +/** + * Provides Base16 encoding and decoding. + * + * <p> + * This class is thread-safe. + * </p> + * + * @since 1.15 + */ +public class Base16 extends BaseNCodec { + + private static final int BYTES_PER_UNENCODED_BLOCK = 1; + private static final int BYTES_PER_ENCODED_BLOCK = 2; + + private final boolean toLowerCase; + private final Charset charset; + + /** + * Creates a Base16 codec used for decoding and encoding. + */ + protected Base16() { + this(Hex.DEFAULT_CHARSET); + } + + /** + * Creates a Base16 codec used for decoding and encoding. + * + * @param charset the charset. + */ + protected Base16(final Charset charset) { + this(true, charset); + } + + /** + * Creates a Base16 codec used for decoding and encoding. + * + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase. + * @param charset the charset. + */ + protected Base16(final boolean toLowerCase, final Charset charset) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, 0, 0); + this.toLowerCase = toLowerCase; + this.charset = charset; + } + + + @Override + void encode(final byte[] data, final int offset, final int length, final Context context) { + if (context.eof) { + return; + } + if (length < 0) { + context.eof = true; + return; + } + + final char[] chars = Hex.encodeHex(data, offset, length, toLowerCase); + final byte[] encoded = new String(chars).getBytes(charset); + + final byte[] buffer = ensureBufferSize(encoded.length, context); + System.arraycopy(encoded, 0, buffer, context.pos, encoded.length); + + context.pos += encoded.length; + } + + @Override + void decode(final byte[] data, final int offset, final int length, final Context context) { + if (context.eof) { + return; + } + if (length < 0) { + context.eof = true; + return; + } + + final int dataLen = Math.min(data.length - offset, length); + final int availableChars = (context.ibitWorkArea > 0 ? 1 : 0) + dataLen; + + // small optimisation to short-cut the rest of this method when it is fed byte-by-byte + if (availableChars == 1 && availableChars == dataLen) { + context.ibitWorkArea = data[offset]; // store 1/2 byte for next invocation of decode + return; + } + + // NOTE: Each pair of bytes is really a pair of hex-chars, therefore each pair represents one byte + + // we must have an even number of chars to decode + final char[] encodedChars = new char[availableChars % 2 == 0 ? availableChars : availableChars - 1]; + + // copy all (or part of) data into encodedChars + int i = 0; + if (dataLen < availableChars) { + // we have 1/2 byte from previous invocation to decode + encodedChars[i++] = (char)context.ibitWorkArea; + context.ibitWorkArea = -1; // reset for next iteration! + } + final int copyLen = encodedChars.length - i; + for (int j = offset; j < copyLen + offset; j++) { + encodedChars[i++] = (char) data[j]; + } + + // decode encodedChars into buffer + final byte[] buffer = ensureBufferSize(encodedChars.length / 2, context); + try { + final int written = Hex.decodeHex(encodedChars, buffer, context.pos); + context.pos += written; + } catch (final DecoderException e) { + throw new RuntimeException(e); // this method ensures that this cannot happen at runtime! + } + + // we have one char of a hex-pair left over + if (copyLen < dataLen) { + context.ibitWorkArea = data[offset + dataLen - 1]; // store 1/2 byte for next invocation of decode + } + } + + @Override + protected boolean isInAlphabet(final byte value) { + if (value >= '0' && value <= '9') { + return true; + } + + if (toLowerCase) { + return value >= 'a' && value <= 'f'; + } else { + return value >= 'A' && value <= 'F'; + } + } + + /** + * Returns whether or not the {@code c} is in the base 16 alphabet. + * + * @param c The value to test + * @return {@code true} if the value is defined in the the base 16 alphabet, {@code false} otherwise. + */ + public static boolean isBase16(final char c) { + return + (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'F') + || (c >= 'a' && c <= 'f'); + } + + /** + * Tests a given String to see if it contains only valid characters within the Base16 alphabet. + * + * @param base16 String to test + * @return {@code true} if all characters in the String are valid characters in the Base16 alphabet or if + * the String is empty; {@code false}, otherwise + */ + public static boolean isBase16(final String base16) { + return isBase16(base16.toCharArray()); + } + + /** + * Tests a given char array to see if it contains only valid characters within the Base16 alphabet. + * + * @param arrayChars char array to test + * @return {@code true} if all chars are valid characters in the Base16 alphabet or if the char array is empty; + * {@code false}, otherwise + */ + public static boolean isBase16(final char[] arrayChars) { + for (int i = 0; i < arrayChars.length; i++) { + if (!isBase16(arrayChars[i])) { + return false; + } + } + return true; + } + + /** + * Tests a given char array to see if it contains only valid characters within the Base16 alphabet. + * + * @param arrayChars byte array to test + * @return {@code true} if all chars are valid characters in the Base16 alphabet or if the byte array is empty; + * {@code false}, otherwise + */ + public static boolean isBase16(final byte[] arrayChars) { + for (int i = 0; i < arrayChars.length; i++) { + if (!isBase16((char) arrayChars[i])) { + return false; + } + } + return true; + } + + /** + * Encodes binary data using the base16 algorithm. + * + * @param binaryData Array containing binary data to encode. + * @return Base16-encoded data. + */ + public static byte[] encodeBase16(final byte[] binaryData) { + return encodeBase16(binaryData, true, Hex.DEFAULT_CHARSET, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base16 algorithm. + * + * @param binaryData Array containing binary data to encode. + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase. + * @param charset the charset. + * @param maxResultSize The maximum result size to accept. + * @return Base16-encoded data. + * @throws IllegalArgumentException Thrown when the input array needs an output array bigger than maxResultSize + */ + public static byte[] encodeBase16(final byte[] binaryData, final boolean toLowerCase, final Charset charset, + final int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and the code + final Base16 b16 = new Base16(toLowerCase, charset); + final long len = b16.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + + len + + ") than the specified maximum size of " + + maxResultSize); + } + + return b16.encode(binaryData); + } + + /** + * Decodes a Base16 String into octets. + * + * @param base16String String containing Base16 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase16(final String base16String) { + return new Base16().decode(base16String); + } + + /** + * Decodes Base16 data into octets. + * + * @param base16Data Byte array containing Base16 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase16(final byte[] base16Data) { + return new Base16().decode(base16Data); + } +} diff --git a/src/main/java/org/apache/commons/codec/binary/Base16InputStream.java b/src/main/java/org/apache/commons/codec/binary/Base16InputStream.java new file mode 100644 index 0000000..94e8cf4 --- /dev/null +++ b/src/main/java/org/apache/commons/codec/binary/Base16InputStream.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * Provides Base16 encoding and decoding in a streaming fashion (unlimited size). + * <p> + * The default behavior of the Base16InputStream is to DECODE, whereas the default behavior of the + * {@link Base16OutputStream} is to ENCODE, but this behavior can be overridden by using a different constructor. + * </p> + * + * @since 1.15 + */ +public class Base16InputStream extends BaseNCodecInputStream { + + /** + * Creates a Base16InputStream such that all data read is Base16-decoded from the original provided InputStream. + * + * @param in InputStream to wrap. + */ + public Base16InputStream(final InputStream in) { + this(in, false); + } + + /** + * Creates a Base16InputStream such that all data read is either Base16-encoded or Base16-decoded from the original + * provided InputStream. + * + * @param in InputStream to wrap. + * @param doEncode true if we should encode all data read from us, false if we should decode. + */ + public Base16InputStream(final InputStream in, final boolean doEncode) { + this(in, doEncode, true, Hex.DEFAULT_CHARSET); + } + + /** + * Creates a Base16InputStream such that all data read is either Base16-encoded or Base16-decoded from the original + * provided InputStream. + * + * @param in InputStream to wrap. + * @param doEncode true if we should encode all data read from us, false if we should decode. + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase. + * @param charset the charset. + */ + public Base16InputStream(final InputStream in, final boolean doEncode, + final boolean toLowerCase, final Charset charset) { + super(in, new Base16(toLowerCase, charset), doEncode); + } +} diff --git a/src/main/java/org/apache/commons/codec/binary/Base16OutputStream.java b/src/main/java/org/apache/commons/codec/binary/Base16OutputStream.java new file mode 100644 index 0000000..e214320 --- /dev/null +++ b/src/main/java/org/apache/commons/codec/binary/Base16OutputStream.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * Provides Hex encoding and decoding in a streaming fashion (unlimited size). + * <p> + * The default behavior of the HexOutputStream is to ENCODE, whereas the default behavior of the + * {@link Base16InputStream} is to DECODE. But this behavior can be overridden by using a different constructor. + * </p> + * + * @since 1.15 + */ +public class Base16OutputStream extends BaseNCodecOutputStream { + + /** + * Creates a Base16OutputStream such that all data written is Hex-encoded to the original provided OutputStream. + * + * @param out OutputStream to wrap. + */ + public Base16OutputStream(final OutputStream out) { + this(out, true); + } + + /** + * Creates a Base16OutputStream such that all data written is either Hex-encoded or Hex-decoded to the + * original provided OutputStream. + * + * @param out OutputStream to wrap. + * @param doEncode true if we should encode all data written to us, false if we should decode. + */ + public Base16OutputStream(final OutputStream out, final boolean doEncode) { + this(out, doEncode, true, Hex.DEFAULT_CHARSET); + } + + /** + * Creates a Base16OutputStream such that all data written is either Hex-encoded or Hex-decoded to the + * original provided OutputStream. + * + * @param out OutputStream to wrap. + * @param doEncode true if we should encode all data written to us, false if we should decode. + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase. + * @param charset the charset. + */ + public Base16OutputStream(final OutputStream out, final boolean doEncode, + final boolean toLowerCase, final Charset charset) { + super(out, new Base16(toLowerCase, charset), doEncode); + } +} diff --git a/src/main/java/org/apache/commons/codec/binary/Hex.java b/src/main/java/org/apache/commons/codec/binary/Hex.java index 57c05bd..9c9a862 100644 --- a/src/main/java/org/apache/commons/codec/binary/Hex.java +++ b/src/main/java/org/apache/commons/codec/binary/Hex.java @@ -54,13 +54,13 @@ public class Hex implements BinaryEncoder, BinaryDecoder { /** * Used to build output as Hex */ - private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * Used to build output as Hex */ - private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', + static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** @@ -73,17 +73,38 @@ public class Hex implements BinaryEncoder, BinaryDecoder { * @throws DecoderException Thrown if an odd number or illegal of characters is supplied */ public static byte[] decodeHex(final char[] data) throws DecoderException { + final byte[] out = new byte[data.length >> 1]; + decodeHex(data, out, 0); + return out; + } + /** + * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The + * returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param data An array of characters containing hexadecimal digits + * @param out A byte array to contain the binary data decoded from the supplied char array. + * @param outOffset The position within {@code out} to start writing the decoded bytes. + * @return the number of bytes written to {@code out}. + * @throws DecoderException Thrown if an odd number or illegal of characters is supplied + * + * @since 1.15 + */ + public static int decodeHex(final char[] data, final byte[] out, final int outOffset) throws DecoderException { final int len = data.length; if ((len & 0x01) != 0) { throw new DecoderException("Odd number of characters."); } - final byte[] out = new byte[len >> 1]; + final int outLen = len >> 1; + if (out.length - outOffset < outLen) { + throw new DecoderException("out is not large enough to accommodate decoded data."); + } // two characters form the hex value. - for (int i = 0, j = 0; j < len; i++) { + for (int i = outOffset, j = 0; j < len; i++) { int f = toDigit(data[j], j) << 4; j++; f = f | toDigit(data[j], j); @@ -91,7 +112,7 @@ public class Hex implements BinaryEncoder, BinaryDecoder { out[i] = (byte) (f & 0xFF); } - return out; + return outLen; } /** @@ -148,12 +169,62 @@ public class Hex implements BinaryEncoder, BinaryDecoder { protected static char[] encodeHex(final byte[] data, final char[] toDigits) { final int l = data.length; final char[] out = new char[l << 1]; + encodeHex(data, 0, data.length, toDigits, out, 0); + return out; + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * + * @param data a byte[] to convert to Hex characters + * @param dataOffset the position in {@code data} to start encoding from + * @param dataLen the number of bytes from {@code dataOffset} to encode + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase + * @return A char[] containing the appropriate characters from the alphabet For best results, this should be either + * upper- or lower-case hex. + * @since 1.15 + */ + protected static char[] encodeHex(final byte[] data, final int dataOffset, final int dataLen, + final boolean toLowerCase) { + final char[] out = new char[dataLen << 1]; + encodeHex(data, dataOffset, dataLen, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER, out, 0); + return out; + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * + * @param data a byte[] to convert to Hex characters + * @param dataOffset the position in {@code data} to start encoding from + * @param dataLen the number of bytes from {@code dataOffset} to encode + * @param toLowerCase {@code true} converts to lowercase, {@code false} to uppercase + * @param out a char[] which will hold the resultant appropriate characters from the alphabet. + * @param outOffset the position within {@code out} at which to start writing the encoded characters. + * @since 1.15 + */ + protected static void encodeHex(final byte[] data, final int dataOffset, final int dataLen, + final boolean toLowerCase, final char[] out, final int outOffset) { + encodeHex(data, dataOffset, dataLen, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER, out, outOffset); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * + * @param data a byte[] to convert to Hex characters + * @param dataOffset the position in {@code data} to start encoding from + * @param dataLen the number of bytes from {@code dataOffset} to encode + * @param toDigits the output alphabet (must contain at least 16 chars) + * @param out a char[] which will hold the resultant appropriate characters from the alphabet. + * @param outOffset the position within {@code out} at which to start writing the encoded characters. + * @since 1.15 + */ + protected static void encodeHex(final byte[] data, final int dataOffset, final int dataLen, final char[] toDigits, + final char[] out, final int outOffset) { // two characters form the hex value. - for (int i = 0, j = 0; i < l; i++) { + for (int i = dataOffset, j = outOffset; i < dataOffset + dataLen; i++) { out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; out[j++] = toDigits[0x0F & data[i]]; } - return out; } /** diff --git a/src/test/java/org/apache/commons/codec/binary/Base16InputStreamTest.java b/src/test/java/org/apache/commons/codec/binary/Base16InputStreamTest.java new file mode 100644 index 0000000..9d4d2a2 --- /dev/null +++ b/src/test/java/org/apache/commons/codec/binary/Base16InputStreamTest.java @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +/** + * @since 1.15 + */ +public class Base16InputStreamTest { + + /** + * Decodes to {202, 254, 186, 190, 255, 255} + */ + private static final String ENCODED_B16 = "cafebabeffff"; + + private static final String STRING_FIXTURE = "Hello World"; + + /** + * Tests skipping past the end of a stream. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testAvailable() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + assertEquals(1, b16Stream.available()); + assertEquals(6, b16Stream.skip(10)); + // End of stream reached + assertEquals(0, b16Stream.available()); + assertEquals(-1, b16Stream.read()); + assertEquals(-1, b16Stream.read()); + assertEquals(0, b16Stream.available()); + } + } + + /** + * Tests the Base16InputStream implementation against empty input. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testBase16EmptyInputStream() throws IOException { + final byte[] emptyEncoded = new byte[0]; + final byte[] emptyDecoded = new byte[0]; + testByteByByte(emptyEncoded, emptyDecoded); + testByChunk(emptyEncoded, emptyDecoded); + } + + /** + * Tests the Base16InputStream implementation. + * + * @throws IOException + * for some failure scenarios. + */ + @Test + public void testBase16InputStreamByChunk() throws IOException { + // Hello World test. + byte[] encoded = StringUtils.getBytesUtf8("48656c6c6f20576f726c64"); + byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + testByChunk(encoded, decoded); + + // Single Byte test. + encoded = StringUtils.getBytesUtf8("41"); + decoded = new byte[] { (byte) 0x41 }; + testByChunk(encoded, decoded); + + // OpenSSL interop test. + encoded = StringUtils.getBytesUtf8(Base16TestData.ENCODED_UTF8_LOWERCASE); + decoded = Base16TestData.DECODED; + testByChunk(encoded, decoded); + + // test random data of sizes 0 thru 150 + for (int i = 0; i <= 150; i++) { + final byte[][] randomData = Base16TestData.randomData(i); + encoded = randomData[1]; + decoded = randomData[0]; + testByChunk(encoded, decoded); + } + } + + /** + * Tests the Base16InputStream implementation. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testBase16InputStreamByteByByte() throws IOException { + // Hello World test. + byte[] encoded = StringUtils.getBytesUtf8("48656c6c6f20576f726c64"); + byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + testByteByByte(encoded, decoded); + + // Single Byte test. + encoded = StringUtils.getBytesUtf8("41"); + decoded = new byte[] { (byte) 0x41 }; + testByteByByte(encoded, decoded); + + // OpenSSL interop test. + encoded = StringUtils.getBytesUtf8(Base16TestData.ENCODED_UTF8_LOWERCASE); + decoded = Base16TestData.DECODED; + testByteByByte(encoded, decoded); + + // test random data of sizes 0 thru 150 + for (int i = 0; i <= 150; i++) { + final byte[][] randomData = Base16TestData.randomData(i); + encoded = randomData[1]; + decoded = randomData[0]; + testByteByByte(encoded, decoded); + } + } + + /** + * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded + * ---[WRAP-WRAP-WRAP-etc...] --> decoded + * <p/> + * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over + * again. + * + * @param encoded Base16 encoded data + * @param decoded the data from above, but decoded + * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation. + */ + private void testByChunk(final byte[] encoded, final byte[] decoded) throws IOException { + + // Start with encode. + InputStream in; + in = new Base16InputStream(new ByteArrayInputStream(decoded), true); + byte[] output = Base16TestData.streamToBytes(in); + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 encode", encoded, output); + + in.close(); + + // Now let's try decode. + in = new Base16InputStream(new ByteArrayInputStream(encoded)); + output = Base16TestData.streamToBytes(in); + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 decode", decoded, output); + + // I always wanted to do this! (wrap encoder with decoder etc etc). + in = new ByteArrayInputStream(decoded); + for (int i = 0; i < 10; i++) { + in = new Base16InputStream(in, true); + in = new Base16InputStream(in, false); + } + output = Base16TestData.streamToBytes(in); + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 wrap-wrap-wrap!", decoded, output); + in.close(); + } + + /** + * Tests method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded + * ---[WRAP-WRAP-WRAP-etc...] --> decoded + * <p/> + * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16InputStream wraps itself in encode and decode mode over and over + * again. + * + * @param encoded Base16 encoded data + * @param decoded the data from above, but decoded + * @throws IOException Usually signifies a bug in the Base16 commons-codec implementation. + */ + private void testByteByByte(final byte[] encoded, final byte[] decoded) throws IOException { + + // Start with encode. + InputStream in; + in = new Base16InputStream(new ByteArrayInputStream(decoded), true); + byte[] output = new byte[encoded.length]; + for (int i = 0; i < output.length; i++) { + output[i] = (byte) in.read(); + } + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 encode", encoded, output); + + in.close(); + // Now let's try decode. + in = new Base16InputStream(new ByteArrayInputStream(encoded)); + output = new byte[decoded.length]; + for (int i = 0; i < output.length; i++) { + output[i] = (byte) in.read(); + } + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 decode", decoded, output); + + in.close(); + + // I always wanted to do this! (wrap encoder with decoder etc etc). + in = new ByteArrayInputStream(decoded); + for (int i = 0; i < 10; i++) { + in = new Base16InputStream(in, true); + in = new Base16InputStream(in, false); + } + output = new byte[decoded.length]; + for (int i = 0; i < output.length; i++) { + output[i] = (byte) in.read(); + } + + assertEquals("EOF", -1, in.read()); + assertEquals("Still EOF", -1, in.read()); + assertArrayEquals("Streaming Base16 wrap-wrap-wrap!", decoded, output); + in.close(); + } + + /** + * Tests markSupported. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testMarkSupported() throws IOException { + final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + final ByteArrayInputStream bin = new ByteArrayInputStream(decoded); + try (final Base16InputStream in = new Base16InputStream(bin, true)) { + // Always returns false for now. + assertFalse("Base16InputStream.markSupported() is false", in.markSupported()); + } + } + + /** + * Tests read returning 0 + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testRead0() throws IOException { + final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + final byte[] buf = new byte[1024]; + int bytesRead = 0; + final ByteArrayInputStream bin = new ByteArrayInputStream(decoded); + try (final Base16InputStream in = new Base16InputStream(bin, true)) { + bytesRead = in.read(buf, 0, 0); + assertEquals("Base16InputStream.read(buf, 0, 0) returns 0", 0, bytesRead); + } + } + + /** + * Tests read with null. + * + * @throws Exception for some failure scenarios. + */ + @Test + public void testReadNull() throws IOException { + final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + final ByteArrayInputStream bin = new ByteArrayInputStream(decoded); + try (final Base16InputStream in = new Base16InputStream(bin, true)) { + in.read(null, 0, 0); + fail("Base16InputStream.read(null, 0, 0) to throw a NullPointerException"); + } catch (final NullPointerException e) { + // Expected + } + } + + /** + * Tests read throwing IndexOutOfBoundsException + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testReadOutOfBounds() throws IOException { + final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + final byte[] buf = new byte[1024]; + final ByteArrayInputStream bin = new ByteArrayInputStream(decoded); + try (final Base16InputStream in = new Base16InputStream(bin, true)) { + + try { + in.read(buf, -1, 0); + fail("Expected Base16InputStream.read(buf, -1, 0) to throw IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // Expected + } + + try { + in.read(buf, 0, -1); + fail("Expected Base16InputStream.read(buf, 0, -1) to throw IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // Expected + } + + try { + in.read(buf, buf.length + 1, 0); + fail("Base16InputStream.read(buf, buf.length + 1, 0) throws IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // Expected + } + + try { + in.read(buf, buf.length - 1, 2); + fail("Base16InputStream.read(buf, buf.length - 1, 2) throws IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // Expected + } + } + } + + /** + * Tests skipping number of characters larger than the internal buffer. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testSkipBig() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + assertEquals(6, b16Stream.skip(Integer.MAX_VALUE)); + // End of stream reached + assertEquals(-1, b16Stream.read()); + assertEquals(-1, b16Stream.read()); + } + } + + /** + * Tests skipping as a noop + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testSkipNone() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + final byte[] actualBytes = new byte[6]; + assertEquals(0, b16Stream.skip(0)); + b16Stream.read(actualBytes, 0, actualBytes.length); + assertArrayEquals(actualBytes, new byte[] {(byte)202, (byte)254, (byte)186, (byte)190, (byte)255, (byte)255}); + // End of stream reached + assertEquals(-1, b16Stream.read()); + } + } + + /** + * Tests skipping past the end of a stream. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testSkipPastEnd() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + // due to CODEC-130, skip now skips correctly decoded characters rather than encoded + assertEquals(6, b16Stream.skip(10)); + // End of stream reached + assertEquals(-1, b16Stream.read()); + assertEquals(-1, b16Stream.read()); + } + } + + /** + * Tests skipping to the end of a stream. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testSkipToEnd() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + // due to CODEC-130, skip now skips correctly decoded characters rather than encoded + assertEquals(6, b16Stream.skip(6)); + // End of stream reached + assertEquals(-1, b16Stream.read()); + assertEquals(-1, b16Stream.read()); + } + } + + /** + * Tests if negative arguments to skip are handled correctly. + * + * @throws IOException for some failure scenarios. + */ + @Test(expected=IllegalArgumentException.class) + public void testSkipWrongArgument() throws IOException { + final InputStream ins = new ByteArrayInputStream(StringUtils.getBytesIso8859_1(ENCODED_B16)); + try (final Base16InputStream b16Stream = new Base16InputStream(ins)) { + b16Stream.skip(-10); + } + } +} diff --git a/src/test/java/org/apache/commons/codec/binary/Base16OutputStreamTest.java b/src/test/java/org/apache/commons/codec/binary/Base16OutputStreamTest.java new file mode 100644 index 0000000..6f91f51 --- /dev/null +++ b/src/test/java/org/apache/commons/codec/binary/Base16OutputStreamTest.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +/** + * @since 1.15 + */ +public class Base16OutputStreamTest { + + private static final String STRING_FIXTURE = "Hello World"; + + /** + * Test the Base16OutputStream implementation against empty input. + * + * @throws IOException for some failure scenarios.. + */ + @Test + public void testBase16EmptyOutputStream() throws IOException { + final byte[] emptyEncoded = new byte[0]; + final byte[] emptyDecoded = new byte[0]; + testByteByByte(emptyEncoded, emptyDecoded); + testByChunk(emptyEncoded, emptyDecoded); + } + + /** + * Test the Base16OutputStream implementation + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testBase16OutputStreamByChunk() throws Exception { + // Hello World test. + byte[] encoded = StringUtils.getBytesUtf8("48656c6c6f20576f726c64"); + byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + testByChunk(encoded, decoded); + + // Single Byte test. + encoded = StringUtils.getBytesUtf8("41"); + decoded = new byte[]{(byte) 0x41}; + testByChunk(encoded, decoded); + + // test random data of sizes 0 thru 150 + for (int i = 0; i <= 150; i++) { + final byte[][] randomData = Base16TestData.randomData(i); + encoded = randomData[1]; + decoded = randomData[0]; + testByChunk(encoded, decoded); + } + } + + /** + * Test the Base16OutputStream implementation + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testBase16OutputStreamByteByByte() throws IOException { + // Hello World test. + byte[] encoded = StringUtils.getBytesUtf8("48656c6c6f20576f726c64"); + byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE); + testByteByByte(encoded, decoded); + + // Single Byte test. + encoded = StringUtils.getBytesUtf8("41"); + decoded = new byte[]{(byte) 0x41}; + testByteByByte(encoded, decoded); + + // test random data of sizes 0 thru 150 + for (int i = 0; i <= 150; i++) { + final byte[][] randomData = Base16TestData.randomData(i); + encoded = randomData[1]; + decoded = randomData[0]; + testByteByByte(encoded, decoded); + } + } + + /** + * Test method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> + * encoded 3. decoded ---[WRAP-WRAP-WRAP-etc...] --> decoded + * <p/> + * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16OutputStream wraps itself in encode and decode + * mode over and over again. + * + * @param encoded + * base16 encoded data + * @param decoded + * the data from above, but decoded + * @throws IOException + * Usually signifies a bug in the Base16 commons-codec implementation. + */ + private void testByChunk(final byte[] encoded, final byte[] decoded) throws IOException { + + // Start with encode. + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + OutputStream out = new Base16OutputStream(byteOut, true); + out.write(decoded); + out.close(); + byte[] output = byteOut.toByteArray(); + assertArrayEquals("Streaming chunked base16 encode", encoded, output); + + // Now let's try decode. + byteOut = new ByteArrayOutputStream(); + out = new Base16OutputStream(byteOut, false); + out.write(encoded); + out.close(); + output = byteOut.toByteArray(); + assertArrayEquals("Streaming chunked base16 decode", decoded, output); + + // I always wanted to do this! (wrap encoder with decoder etc etc). + byteOut = new ByteArrayOutputStream(); + out = byteOut; + for (int i = 0; i < 10; i++) { + out = new Base16OutputStream(out, false); + out = new Base16OutputStream(out, true); + } + out.write(decoded); + out.close(); + output = byteOut.toByteArray(); + + assertArrayEquals("Streaming chunked base16 wrap-wrap-wrap!", decoded, output); + } + + /** + * Test method does three tests on the supplied data: 1. encoded ---[DECODE]--> decoded 2. decoded ---[ENCODE]--> + * encoded 3. decoded ---[WRAP-WRAP-WRAP-etc...] --> decoded + * <p/> + * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the Base16OutputStream wraps itself in encode and decode + * mode over and over again. + * + * @param encoded + * base16 encoded data + * @param decoded + * the data from above, but decoded + * @throws IOException + * Usually signifies a bug in the Base16 commons-codec implementation. + */ + private void testByteByByte(final byte[] encoded, final byte[] decoded) throws IOException { + + // Start with encode. + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + OutputStream out = new Base16OutputStream(byteOut, true); + for (final byte element : decoded) { + out.write(element); + } + out.close(); + byte[] output = byteOut.toByteArray(); + assertArrayEquals("Streaming byte-by-byte base16 encode", encoded, output); + + // Now let's try decode. + byteOut = new ByteArrayOutputStream(); + out = new Base16OutputStream(byteOut, false); + for (final byte element : encoded) { + out.write(element); + } + out.close(); + output = byteOut.toByteArray(); + assertArrayEquals("Streaming byte-by-byte base16 decode", decoded, output); + + // Now let's try decode with tonnes of flushes. + byteOut = new ByteArrayOutputStream(); + out = new Base16OutputStream(byteOut, false); + for (final byte element : encoded) { + out.write(element); + out.flush(); + } + out.close(); + output = byteOut.toByteArray(); + assertArrayEquals("Streaming byte-by-byte flush() base16 decode", decoded, output); + + // I always wanted to do this! (wrap encoder with decoder etc etc). + byteOut = new ByteArrayOutputStream(); + out = byteOut; + for (int i = 0; i < 10; i++) { + out = new Base16OutputStream(out, false); + out = new Base16OutputStream(out, true); + } + for (final byte element : decoded) { + out.write(element); + } + out.close(); + output = byteOut.toByteArray(); + + assertArrayEquals("Streaming byte-by-byte base16 wrap-wrap-wrap!", decoded, output); + } + + /** + * Tests Base16OutputStream.write for expected IndexOutOfBoundsException conditions. + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testWriteOutOfBounds() throws IOException { + final byte[] buf = new byte[1024]; + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (final Base16OutputStream out = new Base16OutputStream(bout)) { + + try { + out.write(buf, -1, 1); + fail("Expected Base16OutputStream.write(buf, -1, 1) to throw a IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException ioobe) { + // Expected + } + + try { + out.write(buf, 1, -1); + fail("Expected Base16OutputStream.write(buf, 1, -1) to throw a IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException ioobe) { + // Expected + } + + try { + out.write(buf, buf.length + 1, 0); + fail("Expected Base16OutputStream.write(buf, buf.length + 1, 0) to throw a IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException ioobe) { + // Expected + } + + try { + out.write(buf, buf.length - 1, 2); + fail("Expected Base16OutputStream.write(buf, buf.length - 1, 2) to throw a IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException ioobe) { + // Expected + } + } + } + + /** + * Tests Base16OutputStream.write(null). + * + * @throws IOException for some failure scenarios. + */ + @Test + public void testWriteToNullCoverage() throws IOException { + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (final Base16OutputStream out = new Base16OutputStream(bout)) { + out.write(null, 0, 0); + fail("Expcted Base16OutputStream.write(null) to throw a NullPointerException"); + } catch (final NullPointerException e) { + // Expected + } + } +} diff --git a/src/test/java/org/apache/commons/codec/binary/Base16Test.java b/src/test/java/org/apache/commons/codec/binary/Base16Test.java new file mode 100644 index 0000000..e0bf357 --- /dev/null +++ b/src/test/java/org/apache/commons/codec/binary/Base16Test.java @@ -0,0 +1,620 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.Assume; +import org.junit.Test; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Random; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test cases for Base16 class. + * + * @since 1.15 + */ +public class Base16Test { + + private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8; + + private final Random random = new Random(); + + /** + * @return Returns the random. + */ + public Random getRandom() { + return this.random; + } + + /** + * Test the isStringBase16 method. + */ + @Test + public void testIsStringBase16() { + final String nullString = null; + final String emptyString = ""; + final String validString = "cafebabec0a1f2e3b4a5b6e7c8"; + final String invalidString = validString + (char) 0; // append null + // character + + try { + Base16.isBase16(nullString); + fail("Base16.isStringBase16() should not be null-safe."); + } catch (final NullPointerException npe) { + assertNotNull("Base16.isStringBase16() should not be null-safe.", npe); + } + + assertTrue("Base16.isStringBase16(empty-string) is true", Base16.isBase16(emptyString)); + assertTrue("Base16.isStringBase16(valid-string) is true", Base16.isBase16(validString)); + assertFalse("Base16.isStringBase16(invalid-string) is false", Base16.isBase16(invalidString)); + } + + /** + * Test the Base16 implementation + */ + @Test + public void testBase16() { + final String content = "Hello World"; + String encodedContent; + byte[] encodedBytes = Base16.encodeBase16(StringUtils.getBytesUtf8(content)); + encodedContent = StringUtils.newStringUtf8(encodedBytes); + assertEquals("encoding hello world", "48656c6c6f20576f726c64", encodedContent); + + Base16 b16 = new Base16(); + encodedBytes = b16.encode(StringUtils.getBytesUtf8(content)); + encodedContent = StringUtils.newStringUtf8(encodedBytes); + assertEquals("encoding hello world", "48656c6c6f20576f726c64", encodedContent); + } + + @Test + public void testBase16AtBufferStart() { + testBase16InBuffer(0, 100); + } + + @Test + public void testBase16AtBufferEnd() { + testBase16InBuffer(100, 0); + } + + @Test + public void testBase16AtBufferMiddle() { + testBase16InBuffer(100, 100); + } + + private void testBase16InBuffer(final int startPasSize, final int endPadSize) { + final String content = "Hello World"; + String encodedContent; + final byte[] bytesUtf8 = StringUtils.getBytesUtf8(content); + byte[] buffer = ArrayUtils.addAll(bytesUtf8, new byte[endPadSize]); + buffer = ArrayUtils.addAll(new byte[startPasSize], buffer); + final byte[] encodedBytes = new Base16().encode(buffer, startPasSize, bytesUtf8.length); + encodedContent = StringUtils.newStringUtf8(encodedBytes); + assertEquals("encoding hello world", "48656c6c6f20576f726c64", encodedContent); + } + + /** + * isBase16 throws RuntimeException on some + * non-Base16 bytes + */ + @Test(expected=RuntimeException.class) + public void testCodec68() { + final byte[] x = new byte[] { 'n', 'H', '=', '=', (byte) 0x9c }; + Base16.decodeBase16(x); + } + + @Test + public void testConstructors() { + new Base16(); + new Base16(); + new Base16(CHARSET_UTF8); + new Base16(false, CHARSET_UTF8); + } + + @Test + public void testConstructor_Charset() { + final Base16 Base16 = new Base16(CHARSET_UTF8); + final byte[] encoded = Base16.encode(Base16TestData.DECODED); + final String expectedResult = Base16TestData.ENCODED_UTF8_LOWERCASE; + final String result = StringUtils.newStringUtf8(encoded); + assertEquals("new Base16(UTF_8)", expectedResult, result); + } + + @Test + public void testConstructor_Boolean_Charset() { + final Base16 Base16 = new Base16(false, CHARSET_UTF8); + final byte[] encoded = Base16.encode(Base16TestData.DECODED); + final String expectedResult = Base16TestData.ENCODED_UTF8_UPPERCASE; + final String result = StringUtils.newStringUtf8(encoded); + assertEquals("new Base16(false, UTF_8)", result, expectedResult); + } + + /** + * Test encode and decode of empty byte array. + */ + @Test + public void testEmptyBase16() { + byte[] empty = new byte[0]; + byte[] result = Base16.encodeBase16(empty); + assertEquals("empty Base16 encode", 0, result.length); + assertEquals("empty Base16 encode", null, Base16.encodeBase16(null)); + + empty = new byte[0]; + result = Base16.decodeBase16(empty); + assertEquals("empty Base16 decode", 0, result.length); + assertEquals("empty Base16 encode", null, Base16.decodeBase16((byte[]) null)); + } + + // encode/decode a large random array + @Test + public void testEncodeDecodeRandom() { + for (int i = 1; i < 5; i++) { + final int len = this.getRandom().nextInt(10000) + 1; + final byte[] data = new byte[len]; + this.getRandom().nextBytes(data); + final byte[] enc = Base16.encodeBase16(data); + assertTrue(Base16.isBase16(enc)); + final byte[] data2 = Base16.decodeBase16(enc); + assertArrayEquals(data, data2); + } + } + + // encode/decode random arrays from size 0 to size 11 + @Test + public void testEncodeDecodeSmall() { + for (int i = 0; i < 12; i++) { + final byte[] data = new byte[i]; + this.getRandom().nextBytes(data); + final byte[] enc = Base16.encodeBase16(data); + assertTrue("\"" + new String(enc) + "\" is Base16 data.", Base16.isBase16(enc)); + final byte[] data2 = Base16.decodeBase16(enc); + assertArrayEquals(toString(data) + " equals " + toString(data2), data, data2); + } + } + + @Test + public void testEncodeOverMaxSize() { + testEncodeOverMaxSize(-1); + testEncodeOverMaxSize(0); + testEncodeOverMaxSize(1); + testEncodeOverMaxSize(2); + } + + private void testEncodeOverMaxSize(final int maxSize) { + try { + Base16.encodeBase16(Base16TestData.DECODED, true, CHARSET_UTF8, maxSize); + fail("Expected " + IllegalArgumentException.class.getName()); + } catch (final IllegalArgumentException e) { + // Expected + } + } + + @Test + public void testIsArrayByteBase16() { + assertFalse(Base16.isBase16(new char[] { (char)Byte.MIN_VALUE })); + assertFalse(Base16.isBase16(new char[] { (char)-125 })); + assertFalse(Base16.isBase16(new char[] { (char)-10 })); + assertFalse(Base16.isBase16(new char[] { 0 })); + assertFalse(Base16.isBase16(new char[] { 64, Byte.MAX_VALUE })); + assertFalse(Base16.isBase16(new char[] { Byte.MAX_VALUE })); + assertTrue(Base16.isBase16(new char[] { 'A' })); + assertFalse(Base16.isBase16(new char[] { 'A', (char)Byte.MIN_VALUE })); + assertTrue(Base16.isBase16(new char[] { 'A', 'F', 'a' })); + assertFalse(Base16.isBase16(new char[] { '/', '=', '+' })); + assertFalse(Base16.isBase16(new char[] { '$' })); + } + + @Test + public void testKnownDecodings() { + assertEquals("The quick brown fox jumped over the lazy dogs.", new String(Base16.decodeBase16( + "54686520717569636b2062726f776e20666f78206a756d706564206f76657220746865206c617a7920646f67732e".getBytes(CHARSET_UTF8)))); + assertEquals("It was the best of times, it was the worst of times.", new String(Base16.decodeBase16( + "497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732e".getBytes(CHARSET_UTF8)))); + assertEquals("http://jakarta.apache.org/commmons", new String( + Base16.decodeBase16("687474703a2f2f6a616b617274612e6170616368652e6f72672f636f6d6d6d6f6e73".getBytes(CHARSET_UTF8)))); + assertEquals("AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz", new String(Base16.decodeBase16( + "4161426243634464456546664767486849694a6a4b6b4c6c4d6d4e6e4f6f50705171527253735474557556765777587859795a7a".getBytes(CHARSET_UTF8)))); + assertEquals("{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }", + new String(Base16.decodeBase16("7b20302c20312c20322c20332c20342c20352c20362c20372c20382c2039207d".getBytes(CHARSET_UTF8)))); + assertEquals("xyzzy!", new String(Base16.decodeBase16("78797a7a7921".getBytes(CHARSET_UTF8)))); + } + + @Test + public void testKnownEncodings() { + assertEquals("54686520717569636b2062726f776e20666f78206a756d706564206f76657220746865206c617a7920646f67732e", new String( + Base16.encodeBase16("The quick brown fox jumped over the lazy dogs.".getBytes(CHARSET_UTF8)))); + assertEquals("497420776173207468652062657374206f662074696d65732c206974207761732074686520776f727374206f662074696d65732e", new String( + Base16.encodeBase16("It was the best of times, it was the worst of times.".getBytes(CHARSET_UTF8)))); + assertEquals("687474703a2f2f6a616b617274612e6170616368652e6f72672f636f6d6d6d6f6e73", + new String(Base16.encodeBase16("http://jakarta.apache.org/commmons".getBytes(CHARSET_UTF8)))); + assertEquals("4161426243634464456546664767486849694a6a4b6b4c6c4d6d4e6e4f6f50705171527253735474557556765777587859795a7a", new String( + Base16.encodeBase16("AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz".getBytes(CHARSET_UTF8)))); + assertEquals("7b20302c20312c20322c20332c20342c20352c20362c20372c20382c2039207d", + new String(Base16.encodeBase16("{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }".getBytes(CHARSET_UTF8)))); + assertEquals("78797a7a7921", new String(Base16.encodeBase16("xyzzy!".getBytes(CHARSET_UTF8)))); + } + + @Test + public void testNonBase16Test() { + final byte[] bArray = { '%' }; + + assertFalse("Invalid Base16 array was incorrectly validated as " + "an array of Base16 encoded data", + Base16.isBase16(bArray)); + + try { + final Base16 b16 = new Base16(); + final byte[] result = b16.decode(bArray); + + assertEquals("The result should be empty as the test encoded content did " + + "not contain any valid base 16 characters", 0, result.length); + } catch (final Exception e) { + fail("Exception was thrown when trying to decode invalid Base16 encoded data"); + } + } + + @Test + public void testObjectDecodeWithInvalidParameter() { + final Base16 b16 = new Base16(); + + try { + b16.decode(Integer.valueOf(5)); + fail("decode(Object) didn't throw an exception when passed an Integer object"); + } catch (final DecoderException e) { + // ignored + } + + } + + @Test + public void testObjectDecodeWithValidParameter() throws Exception { + final String original = "Hello World!"; + final Object o = Base16.encodeBase16(original.getBytes(CHARSET_UTF8)); + + final Base16 b16 = new Base16(); + final Object oDecoded = b16.decode(o); + final byte[] baDecoded = (byte[]) oDecoded; + final String dest = new String(baDecoded); + + assertEquals("dest string does not equal original", original, dest); + } + + @Test + public void testObjectEncodeWithInvalidParameter() { + final Base16 b16 = new Base16(); + try { + b16.encode("Yadayadayada"); + fail("encode(Object) didn't throw an exception when passed a String object"); + } catch (final EncoderException e) { + // Expected + } + } + + @Test + public void testObjectEncodeWithValidParameter() throws Exception { + final String original = "Hello World!"; + final Object origObj = original.getBytes(CHARSET_UTF8); + + final Base16 b16 = new Base16(); + final Object oEncoded = b16.encode(origObj); + final byte[] bArray = Base16.decodeBase16((byte[]) oEncoded); + final String dest = new String(bArray); + + assertEquals("dest string does not equal original", original, dest); + } + + @Test + public void testObjectEncode() { + final Base16 b16 = new Base16(); + assertEquals("48656c6c6f20576f726c64", new String(b16.encode("Hello World".getBytes(CHARSET_UTF8)))); + } + + @Test + public void testPairs() { + assertEquals("0000", new String(Base16.encodeBase16(new byte[] { 0, 0 }))); + for (int i = -128; i <= 127; i++) { + final byte test[] = { (byte) i, (byte) i }; + assertArrayEquals(test, Base16.decodeBase16(Base16.encodeBase16(test))); + } + } + + @Test + public void testSingletons() { + assertEquals("00", new String(Base16.encodeBase16(new byte[] { (byte) 0 }))); + assertEquals("01", new String(Base16.encodeBase16(new byte[] { (byte) 1 }))); + assertEquals("02", new String(Base16.encodeBase16(new byte[] { (byte) 2 }))); + assertEquals("03", new String(Base16.encodeBase16(new byte[] { (byte) 3 }))); + assertEquals("04", new String(Base16.encodeBase16(new byte[] { (byte) 4 }))); + assertEquals("05", new String(Base16.encodeBase16(new byte[] { (byte) 5 }))); + assertEquals("06", new String(Base16.encodeBase16(new byte[] { (byte) 6 }))); + assertEquals("07", new String(Base16.encodeBase16(new byte[] { (byte) 7 }))); + assertEquals("08", new String(Base16.encodeBase16(new byte[] { (byte) 8 }))); + assertEquals("09", new String(Base16.encodeBase16(new byte[] { (byte) 9 }))); + assertEquals("0a", new String(Base16.encodeBase16(new byte[] { (byte) 10 }))); + assertEquals("0b", new String(Base16.encodeBase16(new byte[] { (byte) 11 }))); + assertEquals("0c", new String(Base16.encodeBase16(new byte[] { (byte) 12 }))); + assertEquals("0d", new String(Base16.encodeBase16(new byte[] { (byte) 13 }))); + assertEquals("0e", new String(Base16.encodeBase16(new byte[] { (byte) 14 }))); + assertEquals("0f", new String(Base16.encodeBase16(new byte[] { (byte) 15 }))); + assertEquals("10", new String(Base16.encodeBase16(new byte[] { (byte) 16 }))); + assertEquals("11", new String(Base16.encodeBase16(new byte[] { (byte) 17 }))); + assertEquals("12", new String(Base16.encodeBase16(new byte[] { (byte) 18 }))); + assertEquals("13", new String(Base16.encodeBase16(new byte[] { (byte) 19 }))); + assertEquals("14", new String(Base16.encodeBase16(new byte[] { (byte) 20 }))); + assertEquals("15", new String(Base16.encodeBase16(new byte[] { (byte) 21 }))); + assertEquals("16", new String(Base16.encodeBase16(new byte[] { (byte) 22 }))); + assertEquals("17", new String(Base16.encodeBase16(new byte[] { (byte) 23 }))); + assertEquals("18", new String(Base16.encodeBase16(new byte[] { (byte) 24 }))); + assertEquals("19", new String(Base16.encodeBase16(new byte[] { (byte) 25 }))); + assertEquals("1a", new String(Base16.encodeBase16(new byte[] { (byte) 26 }))); + assertEquals("1b", new String(Base16.encodeBase16(new byte[] { (byte) 27 }))); + assertEquals("1c", new String(Base16.encodeBase16(new byte[] { (byte) 28 }))); + assertEquals("1d", new String(Base16.encodeBase16(new byte[] { (byte) 29 }))); + assertEquals("1e", new String(Base16.encodeBase16(new byte[] { (byte) 30 }))); + assertEquals("1f", new String(Base16.encodeBase16(new byte[] { (byte) 31 }))); + assertEquals("20", new String(Base16.encodeBase16(new byte[] { (byte) 32 }))); + assertEquals("21", new String(Base16.encodeBase16(new byte[] { (byte) 33 }))); + assertEquals("22", new String(Base16.encodeBase16(new byte[] { (byte) 34 }))); + assertEquals("23", new String(Base16.encodeBase16(new byte[] { (byte) 35 }))); + assertEquals("24", new String(Base16.encodeBase16(new byte[] { (byte) 36 }))); + assertEquals("25", new String(Base16.encodeBase16(new byte[] { (byte) 37 }))); + assertEquals("26", new String(Base16.encodeBase16(new byte[] { (byte) 38 }))); + assertEquals("27", new String(Base16.encodeBase16(new byte[] { (byte) 39 }))); + assertEquals("28", new String(Base16.encodeBase16(new byte[] { (byte) 40 }))); + assertEquals("29", new String(Base16.encodeBase16(new byte[] { (byte) 41 }))); + assertEquals("2a", new String(Base16.encodeBase16(new byte[] { (byte) 42 }))); + assertEquals("2b", new String(Base16.encodeBase16(new byte[] { (byte) 43 }))); + assertEquals("2c", new String(Base16.encodeBase16(new byte[] { (byte) 44 }))); + assertEquals("2d", new String(Base16.encodeBase16(new byte[] { (byte) 45 }))); + assertEquals("2e", new String(Base16.encodeBase16(new byte[] { (byte) 46 }))); + assertEquals("2f", new String(Base16.encodeBase16(new byte[] { (byte) 47 }))); + assertEquals("30", new String(Base16.encodeBase16(new byte[] { (byte) 48 }))); + assertEquals("31", new String(Base16.encodeBase16(new byte[] { (byte) 49 }))); + assertEquals("32", new String(Base16.encodeBase16(new byte[] { (byte) 50 }))); + assertEquals("33", new String(Base16.encodeBase16(new byte[] { (byte) 51 }))); + assertEquals("34", new String(Base16.encodeBase16(new byte[] { (byte) 52 }))); + assertEquals("35", new String(Base16.encodeBase16(new byte[] { (byte) 53 }))); + assertEquals("36", new String(Base16.encodeBase16(new byte[] { (byte) 54 }))); + assertEquals("37", new String(Base16.encodeBase16(new byte[] { (byte) 55 }))); + assertEquals("38", new String(Base16.encodeBase16(new byte[] { (byte) 56 }))); + assertEquals("39", new String(Base16.encodeBase16(new byte[] { (byte) 57 }))); + assertEquals("3a", new String(Base16.encodeBase16(new byte[] { (byte) 58 }))); + assertEquals("3b", new String(Base16.encodeBase16(new byte[] { (byte) 59 }))); + assertEquals("3c", new String(Base16.encodeBase16(new byte[] { (byte) 60 }))); + assertEquals("3d", new String(Base16.encodeBase16(new byte[] { (byte) 61 }))); + assertEquals("3e", new String(Base16.encodeBase16(new byte[] { (byte) 62 }))); + assertEquals("3f", new String(Base16.encodeBase16(new byte[] { (byte) 63 }))); + assertEquals("40", new String(Base16.encodeBase16(new byte[] { (byte) 64 }))); + assertEquals("41", new String(Base16.encodeBase16(new byte[] { (byte) 65 }))); + assertEquals("42", new String(Base16.encodeBase16(new byte[] { (byte) 66 }))); + assertEquals("43", new String(Base16.encodeBase16(new byte[] { (byte) 67 }))); + assertEquals("44", new String(Base16.encodeBase16(new byte[] { (byte) 68 }))); + assertEquals("45", new String(Base16.encodeBase16(new byte[] { (byte) 69 }))); + assertEquals("46", new String(Base16.encodeBase16(new byte[] { (byte) 70 }))); + assertEquals("47", new String(Base16.encodeBase16(new byte[] { (byte) 71 }))); + assertEquals("48", new String(Base16.encodeBase16(new byte[] { (byte) 72 }))); + assertEquals("49", new String(Base16.encodeBase16(new byte[] { (byte) 73 }))); + assertEquals("4a", new String(Base16.encodeBase16(new byte[] { (byte) 74 }))); + assertEquals("4b", new String(Base16.encodeBase16(new byte[] { (byte) 75 }))); + assertEquals("4c", new String(Base16.encodeBase16(new byte[] { (byte) 76 }))); + assertEquals("4d", new String(Base16.encodeBase16(new byte[] { (byte) 77 }))); + assertEquals("4e", new String(Base16.encodeBase16(new byte[] { (byte) 78 }))); + assertEquals("4f", new String(Base16.encodeBase16(new byte[] { (byte) 79 }))); + assertEquals("50", new String(Base16.encodeBase16(new byte[] { (byte) 80 }))); + assertEquals("51", new String(Base16.encodeBase16(new byte[] { (byte) 81 }))); + assertEquals("52", new String(Base16.encodeBase16(new byte[] { (byte) 82 }))); + assertEquals("53", new String(Base16.encodeBase16(new byte[] { (byte) 83 }))); + assertEquals("54", new String(Base16.encodeBase16(new byte[] { (byte) 84 }))); + assertEquals("55", new String(Base16.encodeBase16(new byte[] { (byte) 85 }))); + assertEquals("56", new String(Base16.encodeBase16(new byte[] { (byte) 86 }))); + assertEquals("57", new String(Base16.encodeBase16(new byte[] { (byte) 87 }))); + assertEquals("58", new String(Base16.encodeBase16(new byte[] { (byte) 88 }))); + assertEquals("59", new String(Base16.encodeBase16(new byte[] { (byte) 89 }))); + assertEquals("5a", new String(Base16.encodeBase16(new byte[] { (byte) 90 }))); + assertEquals("5b", new String(Base16.encodeBase16(new byte[] { (byte) 91 }))); + assertEquals("5c", new String(Base16.encodeBase16(new byte[] { (byte) 92 }))); + assertEquals("5d", new String(Base16.encodeBase16(new byte[] { (byte) 93 }))); + assertEquals("5e", new String(Base16.encodeBase16(new byte[] { (byte) 94 }))); + assertEquals("5f", new String(Base16.encodeBase16(new byte[] { (byte) 95 }))); + assertEquals("60", new String(Base16.encodeBase16(new byte[] { (byte) 96 }))); + assertEquals("61", new String(Base16.encodeBase16(new byte[] { (byte) 97 }))); + assertEquals("62", new String(Base16.encodeBase16(new byte[] { (byte) 98 }))); + assertEquals("63", new String(Base16.encodeBase16(new byte[] { (byte) 99 }))); + assertEquals("64", new String(Base16.encodeBase16(new byte[] { (byte) 100 }))); + assertEquals("65", new String(Base16.encodeBase16(new byte[] { (byte) 101 }))); + assertEquals("66", new String(Base16.encodeBase16(new byte[] { (byte) 102 }))); + assertEquals("67", new String(Base16.encodeBase16(new byte[] { (byte) 103 }))); + assertEquals("68", new String(Base16.encodeBase16(new byte[] { (byte) 104 }))); + for (int i = -128; i <= 127; i++) { + final byte test[] = { (byte) i }; + assertTrue(Arrays.equals(test, Base16.decodeBase16(Base16.encodeBase16(test)))); + } + } + + @Test + public void testTriplets() { + assertEquals("000000", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 0 }))); + assertEquals("000001", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 1 }))); + assertEquals("000002", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 2 }))); + assertEquals("000003", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 3 }))); + assertEquals("000004", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 4 }))); + assertEquals("000005", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 5 }))); + assertEquals("000006", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 6 }))); + assertEquals("000007", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 7 }))); + assertEquals("000008", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 8 }))); + assertEquals("000009", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 9 }))); + assertEquals("00000a", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 10 }))); + assertEquals("00000b", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 11 }))); + assertEquals("00000c", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 12 }))); + assertEquals("00000d", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 13 }))); + assertEquals("00000e", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 14 }))); + assertEquals("00000f", new String(Base16.encodeBase16(new byte[] { (byte) 0, (byte) 0, (byte) 15 }))); + } + + @Test + public void testByteToStringVariations() throws DecoderException { + final Base16 Base16 = new Base16(); + final byte[] b1 = StringUtils.getBytesUtf8("Hello World"); + final byte[] b2 = new byte[0]; + final byte[] b3 = null; + + assertEquals("byteToString Hello World", "48656c6c6f20576f726c64", Base16.encodeToString(b1)); + assertEquals("byteToString static Hello World", "48656c6c6f20576f726c64", StringUtils.newStringUtf8(Base16.encodeBase16(b1))); + assertEquals("byteToString \"\"", "", Base16.encodeToString(b2)); + assertEquals("byteToString static \"\"", "", StringUtils.newStringUtf8(Base16.encodeBase16(b2))); + assertEquals("byteToString null", null, Base16.encodeToString(b3)); + assertEquals("byteToString static null", null, StringUtils.newStringUtf8(Base16.encodeBase16(b3))); + } + + @Test + public void testStringToByteVariations() throws DecoderException { + final Base16 Base16 = new Base16(); + final String s1 = "48656c6c6f20576f726c64"; + final String s2 = ""; + final String s3 = null; + + assertEquals("StringToByte Hello World", "Hello World", StringUtils.newStringUtf8(Base16.decode(s1))); + assertEquals("StringToByte Hello World", "Hello World", + StringUtils.newStringUtf8((byte[]) Base16.decode((Object) s1))); + assertEquals("StringToByte static Hello World", "Hello World", + StringUtils.newStringUtf8(Base16.decodeBase16(s1))); + assertEquals("StringToByte \"\"", "", StringUtils.newStringUtf8(Base16.decode(s2))); + assertEquals("StringToByte static \"\"", "", StringUtils.newStringUtf8(Base16.decodeBase16(s2))); + assertEquals("StringToByte null", null, StringUtils.newStringUtf8(Base16.decode(s3))); + assertEquals("StringToByte static null", null, StringUtils.newStringUtf8(Base16.decodeBase16(s3))); + } + + private String toString(final byte[] data) { + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < data.length; i++) { + buf.append(data[i]); + if (i != data.length - 1) { + buf.append(","); + } + } + return buf.toString(); + } + + /** + * Test for CODEC-265: Encode a 1GiB file. + * + * @see <a href="https://issues.apache.org/jira/projects/CODEC/issues/CODEC-265">CODEC-265</a> + */ + @Test(expected = IllegalArgumentException.class) + public void testCodec265_over() { + // 1GiB file to encode: 2^30 bytes + final int size1GiB = 1 << 30; + + // Expecting a size of 2 output bytes per 1 input byte + final int blocks = size1GiB; + final int expectedLength = 2 * blocks; + + // This test is memory hungry. Check we can run it. + final long presumableFreeMemory = BaseNCodecTest.getPresumableFreeMemory(); + + // Estimate the maximum memory required: + // 1GiB + 1GiB + ~2GiB + ~1.33GiB + 32 KiB = ~5.33GiB + // + // 1GiB: Input buffer to encode + // 1GiB: Existing working buffer (due to doubling of default buffer size of 8192) + // ~2GiB: New working buffer to allocate (due to doubling) + // ~1.33GiB: Expected output size (since the working buffer is copied at the end) + // 32KiB: Some head room + final long estimatedMemory = (long) size1GiB * 4 + expectedLength + 32 * 1024; + Assume.assumeTrue("Not enough free memory for the test", presumableFreeMemory > estimatedMemory); + + final byte[] bytes = new byte[size1GiB]; + final byte[] encoded = Base16.encodeBase16(bytes); + assertEquals(expectedLength, encoded.length); + } + + @Test + public void testIsInAlphabet() { + // lower-case + Base16 b16 = new Base16(true, CHARSET_UTF8); + for (char c = '0'; c <= '9'; c++) { + assertTrue(b16.isInAlphabet((byte) c)); + } + for (char c = 'a'; c <= 'f'; c++) { + assertTrue(b16.isInAlphabet((byte) c)); + } + for (char c = 'A'; c <= 'F'; c++) { + assertFalse(b16.isInAlphabet((byte) c)); + } + assertFalse(b16.isInAlphabet((byte) ('0' - 1))); + assertFalse(b16.isInAlphabet((byte) ('9' + 1))); + assertFalse(b16.isInAlphabet((byte) ('a' - 1))); + assertFalse(b16.isInAlphabet((byte) ('z' + 1))); + + // upper-case + b16 = new Base16(false, CHARSET_UTF8); + for (char c = '0'; c <= '9'; c++) { + assertTrue(b16.isInAlphabet((byte) c)); + } + for (char c = 'a'; c <= 'f'; c++) { + assertFalse(b16.isInAlphabet((byte) c)); + } + for (char c = 'A'; c <= 'F'; c++) { + assertTrue(b16.isInAlphabet((byte) c)); + } + assertFalse(b16.isInAlphabet((byte) ('0' - 1))); + assertFalse(b16.isInAlphabet((byte) ('9' + 1))); + assertFalse(b16.isInAlphabet((byte) ('A' - 1))); + assertFalse(b16.isInAlphabet((byte) ('F' + 1))); + } + + @Test + public void testDecodeSingleBytes() { + final String encoded = "556e74696c206e6578742074696d6521"; + + final BaseNCodec.Context context = new BaseNCodec.Context(); + final Base16 b16 = new Base16(); + + final byte[] encocdedBytes = StringUtils.getBytesUtf8(encoded); + + // decode byte-by-byte + b16.decode(encocdedBytes, 0, 1, context); + b16.decode(encocdedBytes, 1, 1, context); // yields "U" + b16.decode(encocdedBytes, 2, 1, context); + b16.decode(encocdedBytes, 3, 1, context); // yields "n" + + // decode split hex-pairs + b16.decode(encocdedBytes, 4, 3, context); // yields "t" + b16.decode(encocdedBytes, 7, 3, context); // yields "il" + b16.decode(encocdedBytes, 10, 3, context); // yields " " + + // decode remaining + b16.decode(encocdedBytes, 13, 19, context); // yields "next time!" + + final byte[] decodedBytes = new byte[context.pos]; + System.arraycopy(context.buffer, context.readPos, decodedBytes, 0, decodedBytes.length); + final String decoded = StringUtils.newStringUtf8(decodedBytes); + + assertEquals("Until next time!", decoded); + } +} diff --git a/src/test/java/org/apache/commons/codec/binary/Base16TestData.java b/src/test/java/org/apache/commons/codec/binary/Base16TestData.java new file mode 100644 index 0000000..575df32 --- /dev/null +++ b/src/test/java/org/apache/commons/codec/binary/Base16TestData.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.codec.binary; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +/** + * This random data was encoded by OpenSSL. Java had nothing to do with it. This data helps us test interop between + * Commons-Codec and OpenSSL. + * + * @since 1.15 + */ +public class Base16TestData { + + final static String ENCODED_UTF8_LOWERCASE + = "f483cd2b052f74b888029e9cb73d764a2426386b2d5b262f48f79ebee7c386bcdac2ceb9be8ca42a36c88f7dd85936bdc40edcfc51f2a56725ad9850ee89df737244f77049e5f4f847dcc011d8db8f2d61bf8658367113e1625e5cc2c9ff9a7ea81a53b0fa5ea56f03355632d5cd36ff5c320be92003a0af45477d712aff96df3c00476c4d5e063029f5f84c2e02261d8afc6ece7f9c2ccf2ada37b0aa5239dad3fd27b0acf2fa86ef5b3af960042cabe6fd4a2fbf268e8be39d3147e343424b88b907bbaa7d3b0520bd0aa20cacc4bff02e828d1d4cf67360613208fe4656b95edd041d81c8881e7a5d7785544cf [...] + + final static String ENCODED_UTF8_UPPERCASE + = "F483CD2B052F74B888029E9CB73D764A2426386B2D5B262F48F79EBEE7C386BCDAC2CEB9BE8CA42A36C88F7DD85936BDC40EDCFC51F2A56725AD9850EE89DF737244F77049E5F4F847DCC011D8DB8F2D61BF8658367113E1625E5CC2C9FF9A7EA81A53B0FA5EA56F03355632D5CD36FF5C320BE92003A0AF45477D712AFF96DF3C00476C4D5E063029F5F84C2E02261D8AFC6ECE7F9C2CCF2ADA37B0AA5239DAD3FD27B0ACF2FA86EF5B3AF960042CABE6FD4A2FBF268E8BE39D3147E343424B88B907BBAA7D3B0520BD0AA20CACC4BFF02E828D1D4CF67360613208FE4656B95EDD041D81C8881E7A5D7785544CF [...] + + final static byte[] DECODED + = {-12, -125, -51, 43, 5, 47, 116, -72, -120, 2, -98, -100, -73, 61, 118, 74, 36, 38, 56, 107, 45, 91, 38, + 47, 72, -9, -98, -66, -25, -61, -122, -68, -38, -62, -50, -71, -66, -116, -92, 42, 54, -56, -113, 125, + -40, 89, 54, -67, -60, 14, -36, -4, 81, -14, -91, 103, 37, -83, -104, 80, -18, -119, -33, 115, 114, 68, + -9, 112, 73, -27, -12, -8, 71, -36, -64, 17, -40, -37, -113, 45, 97, -65, -122, 88, 54, 113, 19, -31, 98, + 94, 92, -62, -55, -1, -102, 126, -88, 26, 83, -80, -6, 94, -91, 111, 3, 53, 86, 50, -43, -51, 54, -1, 92, + 50, 11, -23, 32, 3, -96, -81, 69, 71, 125, 113, 42, -1, -106, -33, 60, 0, 71, 108, 77, 94, 6, 48, 41, -11, + -8, 76, 46, 2, 38, 29, -118, -4, 110, -50, 127, -100, 44, -49, 42, -38, 55, -80, -86, 82, 57, -38, -45, + -3, 39, -80, -84, -14, -6, -122, -17, 91, 58, -7, 96, 4, 44, -85, -26, -3, 74, 47, -65, 38, -114, -117, + -29, -99, 49, 71, -29, 67, 66, 75, -120, -71, 7, -69, -86, 125, 59, 5, 32, -67, 10, -94, 12, -84, -60, -65, + -16, 46, -126, -115, 29, 76, -10, 115, 96, 97, 50, 8, -2, 70, 86, -71, 94, -35, 4, 29, -127, -56, -120, + 30, 122, 93, 119, -123, 84, 76, -15, -111, 81, -75, -34, 41, -72, 126, -7, 77, -33, 108, -110, 39, -125, + -5, 16, 92, -51, -56, 96, 28, -116, 103, -68, 109, -12, 117, -110, -44, -75, 28, 69, -44, 59, 62, -68, + 39, -4, -119, 80, 91, 19, -116, 122, -81, -118, 100, -108, -88, 2, -8, -106, -75, -37, 30, -83, 124, -121, + 108, -127, 26, -1, -8, 102, -81, -118, 127, -113, -51, 36, -46, 15, 106, -33, -104, 106, -43, -84, -122, + 51, -33, 124, -32, 2, -45, 73, -90, 124, 89, -20, -123, 109, -100, 117, 11, 16, -65, 66, -118, -97, -9, + 101, 7, -1, 41, 65, 70, 116, -119, 54, 126, 44, 75, 74, 26, -34, -27, 27, 54, -13, -89, -90, 64, 120, 15, + -43, 123, 82, -33, 90, -74, 41, -62, 38, -68, 62, -62, 34, 92, 50, 95, -67, -110, -99, -71, -44, -123, + 49, 4, 96, 56, 113, 76, 97, -47, -26, -79, -109, 115, -125, 90, 124, 8, -9, -111, 36, -74, 101, -114, 43, + 0, -110, 63, 76, 99, 91, 2, 12, -60, 56, -14, -125, 0, 6, -27, 31, 31, -109, -47, -3, 109, 88, -75, -74, + 19, 26, -66, 110, 39, 13, -50, 47, 104, -38, 18, 19, 84, 103, 100, -42, 48, 110, 37, 21, -107, 83, -52, + -12, 71, 37, -68, -107, -109, 89, -34, -94, -127, 103, -128, -48, -52, 71, 0, 15, 34, 56, -50, 85, -98, + 106, -87, -3, 97, -116, -19, 64, -22, -25, -38, -63, 33, -45, 80, 10, -121, -109, 37, -96, 36, 18, -48, + 46, 44, -66, 115, -94, 3, -102, -27, -17, -116, -51, 88, -17, 7, -109, 24, 66, 83, -91, 105, -92, -19, + 66, -76, 64, -91, 118, -71, 103, -123, 95, 17, -87, -18, -11, 66, -74, 126, 45, 83, -14, 50, 79, 20, 45, + -113, -103, 119, -101, -58, -99, 27, -100, -17, -107, 91, -26, -32, -56, 71, 72, 34, 66, 16, 9, -90, 106, + -44, -62, -106, 11, 114, -82, -120, -28, -67, 4, -99, 109, -20, -19, 0, -40, -110, -119, 42, -6, 4, -31, + 67, 110, -105, 53, 118, 76, 96, -126, -8, -96, 39, -102, 52, 106, 64, 26, -105, -108, -103, -96, -116, + 116, 0, -96, 115, 89, 40, -23, -102, -2, -30, 16, 58, -53, -33, 14, 122, -94, 113, -121, 67, -103, -4, + -126, 98, -27, 124, -12, 120, -64, -44, 127, 45, -120, 50, 124, -27, 87, -20, -84, 81, -35, 113, -77, + -64, -96, -48, -87, -117, -82, 90, -64, -108, -121, 125, -45, -50, -44, -48, -50, 52, -30, -66, -7, 46, + -40, -47, 85, -44, -126, -122, 24, -84, 21, 120, 99, -74, 27, 11, -52, 32, -2, 122, -100, -118, 106, -9, + -106, 109, -19, 71, 42, 126, 66, -56, 10, -51, -44, 68, 109, -13, 81, -109, 65, 121, 60, -68, -117, 126, + -59, 4, -107, -22, 99, -77, 84, 29, 87, 119, -60, 87, 82, -55, -74, 44, -80, 3, 123, -101, 84, -44, 9, 71, + 24, 91, 99, 22, -65, 11, -11, -14, -38, -84, 105, -101, -85, -17, 116, -65, 118, -105, 122, -75, 113, + -57, -81, -33, -110, 28, 104, -24, -110, -57, -78, 38, -5, -15, -79, 87, 105, 85, 41, -42, -114, -67, + -123, 70, 12, 61, 115, 5, 23, -70, 99, 96, -80, 65, -65, 105, -45, -49, 37, -33, -1, 119, -88, 100, 121, + -25, -35, -51, 10, 43, -113, 61, 103, 44, 13, 108, 20, 74, 19, 53, 19, 37, -76, 20, -43, -11, 23, -58, -25, + -52, 121, -40, -118, 58, 50, 19, -8, -33, -30, -49, -27, -11, -80, 93, -17, 34, 93, 69, 100, 66, -54, 40, + 118, 89, -52, -87, 2, 35, -120, 18, 64, 108, 31, -25, 66, 78, 6, -91, -69, -53, 17, 14, -125, 33, -31, -110, + 1, 5, -40, 7, 126, -122, 84, -55, -62, -22, 69, -28, 5, 45, -106, 120, 74, 94, 51, 74, 108, -19, -26, -12, + 49, 64, 88, 68, 41, -65, 126, 125, -1, -8, -83, -67, 74, 2, -114, -80, -119, -9, -89, -125, 21, 95, 34, + -58, -74, 111, -103, 99, 95, 48, 42, 94, -50, -55, -112, -5, -26, 11, -89, -38, -19, 126, 25, 102, 119, + 81, -94, 70, -79, 98, 91, -73, 114, 15, 14, 87, -21, -122, -1, -90, 0, 29, -104, -91, -93, -58, -83, -48, + -22, 100, -112, -41, 77, 22, -24, 112, -72, 105, 100, 6, -86, -39, 40, -43, 35, -2, 4, -94, 97, -121, 52, + -22, 1, 127, -81, -4, -6, -119, 96, 35, -91, 114, 81, 91, 90, -86, -36, 34, -39, 93, -42, 69, 103, -11, + 107, -87, 119, -107, -114, -45, -128, -69, 96}; + + static byte[] streamToBytes(final InputStream is) throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + final byte[] buf = new byte[4096]; + int read; + while ((read = is.read(buf)) > -1) { + os.write(buf, 0, read); + } + return os.toByteArray(); + } + + /** + * Returns an encoded and decoded copy of the same random data. + * + * @param size amount of random data to generate and encode + * @return two byte[] arrays: [0] = decoded, [1] = encoded + */ + static byte[][] randomData(final int size) { + final Random r = new Random(); + final byte[] decoded = new byte[size]; + r.nextBytes(decoded); + final char[] encodedChars = Hex.encodeHex(decoded); + final byte[] encoded = new String(encodedChars).getBytes(Hex.DEFAULT_CHARSET); + return new byte[][] {decoded, encoded}; + } +} diff --git a/src/test/java/org/apache/commons/codec/binary/HexTest.java b/src/test/java/org/apache/commons/codec/binary/HexTest.java index 4c29548..324aaa0 100644 --- a/src/test/java/org/apache/commons/codec/binary/HexTest.java +++ b/src/test/java/org/apache/commons/codec/binary/HexTest.java @@ -17,6 +17,7 @@ package org.apache.commons.codec.binary; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -340,6 +341,18 @@ public class HexTest { checkDecodeHexCharArrayOddCharacters(new char[] { 'A', 'B', 'C', 'D', 'E' }); } + @Test(expected = DecoderException.class) + public void testDecodeHexCharArrayOutBufferUnderSized() throws DecoderException { + final byte[] out = new byte[4]; + Hex.decodeHex("aabbccddeeff".toCharArray(), out, 0); + } + + @Test(expected = DecoderException.class) + public void testDecodeHexCharArrayOutBufferUnderSizedByOffset() throws DecoderException { + final byte[] out = new byte[6]; + Hex.decodeHex("aabbccddeeff".toCharArray(), out, 1); + } + @Test public void testDecodeHexStringOddCharacters() { try { @@ -441,6 +454,27 @@ public class HexTest { } @Test + public void testEncodeDecodeHexCharArrayRandomToOutput() throws DecoderException, EncoderException { + final Random random = new Random(); + for (int i = 5; i > 0; i--) { + final byte[] data = new byte[random.nextInt(10000) + 1]; + random.nextBytes(data); + + // lower-case + final char[] lowerEncodedChars = new char[data.length * 2]; + Hex.encodeHex(data, 0, data.length, true, lowerEncodedChars, 0); + final byte[] decodedLowerCaseBytes = Hex.decodeHex(lowerEncodedChars); + assertArrayEquals(data, decodedLowerCaseBytes); + + // upper-case + final char[] upperEncodedChars = new char[data.length * 2]; + Hex.encodeHex(data, 0, data.length, false, upperEncodedChars, 0); + final byte[] decodedUpperCaseBytes = Hex.decodeHex(upperEncodedChars); + assertArrayEquals(data, decodedUpperCaseBytes); + } + } + + @Test public void testEncodeHexByteArrayEmpty() { assertTrue(Arrays.equals(new char[0], Hex.encodeHex(new byte[0]))); assertTrue(Arrays.equals(new byte[0], new Hex().encode(new byte[0])));