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])));

Reply via email to