This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git


The following commit(s) were added to refs/heads/master by this push:
     new bb3c354c [Codec 326] Add Base58 support (#422)
bb3c354c is described below

commit bb3c354ce9738667858b97c799fef06980a44c13
Author: Inkeet <[email protected]>
AuthorDate: Sat Feb 7 19:00:41 2026 +0530

    [Codec 326] Add Base58 support (#422)
    
    * [CODEC-326] Adds encoding for Base58;
    
    * [CODEC-326] Adds decoding for Base58;
    
    * [CODEC-326] Adds test cases for Base58;
    
    * [CODEC-326] Adds hex encoding and decoding test case for Base58;
    
    * [CODEC-326] Adds Base58InputStream implementation along with tests;
    
    * [CODEC-326] Adds Base58OutputStream implementation along with tests;
    
    * [CODEC-326] Adds javadoc for Base58 and Base58OutputStream;
    
    * [CODEC-326] Fixes linting issues;
    
    * [CODEC-326] Adds Test Vectors test case for Base58;
    
    * [CODEC-326] Fixes compilation issues; Removes redundant encoding method 
setting;
    
    * Fix Javadoc since tag
    
    * Fix Javadoc since tags
    
    * Fix Javadoc since tag
    
    Removed the since tag from the Builder class documentation.
    
    * Fix Javadoc since tags
    
    * Fix Javadoc since tag
    
    * Fix Javadoc since tag
    
    * Fix Javadoc since tag
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
---
 .../org/apache/commons/codec/binary/Base58.java    | 325 ++++++++++++++++++
 .../commons/codec/binary/Base58InputStream.java    | 108 ++++++
 .../commons/codec/binary/Base58OutputStream.java   | 122 +++++++
 .../codec/binary/Base58InputStreamTest.java        | 373 +++++++++++++++++++++
 .../codec/binary/Base58OutputStreamTest.java       | 181 ++++++++++
 .../apache/commons/codec/binary/Base58Test.java    | 262 +++++++++++++++
 6 files changed, 1371 insertions(+)

diff --git a/src/main/java/org/apache/commons/codec/binary/Base58.java 
b/src/main/java/org/apache/commons/codec/binary/Base58.java
new file mode 100644
index 00000000..f94938b2
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base58.java
@@ -0,0 +1,325 @@
+/*
+ * 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
+ *
+ *      https://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.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Provides Base58 encoding and decoding as commonly used in cryptocurrency 
and blockchain applications.
+ * <p>
+ * Base58 is a binary-to-text encoding scheme that uses a 58-character 
alphabet to encode data. It avoids
+ * characters that can be confused (0/O, I/l, +/) and is commonly used in 
Bitcoin and other blockchain systems.
+ * </p>
+ * <p>
+ * This implementation accumulates data internally until EOF is signaled, at 
which point the entire input is
+ * converted using BigInteger arithmetic. This is necessary because Base58 
encoding/decoding requires access
+ * to the complete data to properly handle leading zeros.
+ * </p>
+ * <p>
+ * This class is thread-safe for read operations but the Context object used 
during encoding/decoding should
+ * not be shared between threads.
+ * </p>
+ * <p>
+ * The Base58 alphabet is: 
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
+ * (excludes: 0, I, O, l)
+ * </p>
+ *
+ * @see Base58InputStream
+ * @see Base58OutputStream
+ * @since 1.22.0
+ */
+public class Base58 extends BaseNCodec {
+
+    /**
+     * Base58 alphabet: 
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
+     * (excludes: 0, I, O, l)
+     */
+    private static final byte[] ENCODE_TABLE = {
+            '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 
'E', 'F', 'G', 'H',
+            'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 
'X', 'Y', 'Z', 'a',
+            'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 
'p', 'q', 'r', 's',
+            't', 'u', 'v', 'w', 'x', 'y', 'z'
+    };
+    /**
+     * This array is a lookup table that translates Unicode characters drawn 
from the "Base58 Alphabet"
+     * into their numeric equivalents (0-57). Characters that are not in the 
Base58 alphabet are marked
+     * with -1.
+     */
+    private static final byte[] DECODE_TABLE = {
+            //  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 
00-0f
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 
10-1f
+            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 
20-2f
+            -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, // 30-3f 
'1'-'9' -> 0-8
+            -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, // 
40-4f 'A'-'N', 'P'-'Z' (skip 'I' and 'O')
+            22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,                     // 
50-5a 'P'-'Z'
+            -1, -1, -1, -1, -1, // 5b-5f
+            -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, // 
60-6f 'a'-'k', 'm'-'o' (skip 'l')
+            47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,                     // 
70-7a 'p'-'z'
+    };
+    private final transient Map<Context, byte[]> accumulated = new 
WeakHashMap<>();
+
+    /**
+     * Constructs a Base58 codec used for encoding and decoding.
+     */
+    public Base58() {
+        this(new Builder());
+    }
+
+    /**
+     * Constructs a Base58 codec used for encoding and decoding with custom 
configuration.
+     *
+     * @param builder the builder with custom configuration
+     */
+    public Base58(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Decodes the given Base58 encoded data.
+     * <p>
+     * This implementation accumulates data internally. When length &lt; 0 
(EOF), the accumulated
+     * data is converted from Base58 to binary.
+     * </p>
+     *
+     * @param array   the byte array containing Base58 encoded data
+     * @param offset  the offset in the array to start from
+     * @param length  the number of bytes to decode, or negative to signal EOF
+     * @param context the context for this decoding operation
+     */
+    @Override
+    void decode(byte[] array, int offset, int length, Context context) {
+        if (context.eof) {
+            return;
+        }
+
+        if (length < 0) {
+            context.eof = true;
+            final byte[] accumulate = accumulated.getOrDefault(context, new 
byte[0]);
+            if (accumulate.length > 0) {
+                convertFromBase58(accumulate, context);
+            }
+            accumulated.remove(context);
+            return;
+        }
+
+        final byte[] accumulate = accumulated.getOrDefault(context, new 
byte[0]);
+        final byte[] newAccumulated = new byte[accumulate.length + length];
+        if (accumulate.length > 0) {
+            System.arraycopy(accumulate, 0, newAccumulated, 0, 
accumulate.length);
+        }
+        System.arraycopy(array, offset, newAccumulated, accumulate.length, 
length);
+        accumulated.put(context, newAccumulated);
+    }
+
+    /**
+     * Encodes the given binary data as Base58.
+     * <p>
+     * This implementation accumulates data internally. When length &lt; 0 
(EOF), the accumulated
+     * data is converted to Base58.
+     * </p>
+     *
+     * @param array   the byte array containing binary data to encode
+     * @param offset  the offset in the array to start from
+     * @param length  the number of bytes to encode, or negative to signal EOF
+     * @param context the context for this encoding operation
+     */
+    @Override
+    void encode(byte[] array, int offset, int length, Context context) {
+        if (context.eof) {
+            return;
+        }
+
+        if (length < 0) {
+            context.eof = true;
+            final byte[] accumulate = accumulated.getOrDefault(context, new 
byte[0]);
+            convertToBase58(accumulate, context);
+            accumulated.remove(context);
+            return;
+        }
+
+        final byte[] accumulate = accumulated.getOrDefault(context, new 
byte[0]);
+        final byte[] newAccumulated = new byte[accumulate.length + length];
+        if (accumulate.length > 0) {
+            System.arraycopy(accumulate, 0, newAccumulated, 0, 
accumulate.length);
+        }
+        System.arraycopy(array, offset, newAccumulated, accumulate.length, 
length);
+        accumulated.put(context, newAccumulated);
+    }
+
+    /**
+     * Converts accumulated binary data to Base58 encoding.
+     * <p>
+     * Uses BigInteger arithmetic to convert the binary data to Base58. 
Leading zeros in the
+     * binary data are represented as '1' characters in the Base58 encoding.
+     * </p>
+     *
+     * @param accumulate the binary data to encode
+     * @param context    the context for this encoding operation
+     * @return the buffer containing the encoded data
+     */
+    private byte[] convertToBase58(byte[] accumulate, Context context) {
+        final StringBuilder base58 = getStringBuilder(accumulate);
+        final String encoded = base58.reverse().toString();
+
+        final byte[] encodedBytes = encoded.getBytes(StandardCharsets.UTF_8);
+        final byte[] buffer = ensureBufferSize(encodedBytes.length, context);
+        System.arraycopy(encodedBytes, 0, buffer, context.pos, 
encodedBytes.length);
+        context.pos += encodedBytes.length;
+        return buffer;
+    }
+
+    /**
+     * Builds the Base58 string representation of the given binary data.
+     * <p>
+     * Converts binary data to a BigInteger and divides by 58 repeatedly to 
get the Base58 digits.
+     * Handles leading zeros by counting them and appending '1' for each 
leading zero byte.
+     * </p>
+     *
+     * @param accumulate the binary data to convert
+     * @return a StringBuilder with the Base58 representation (not yet 
reversed)
+     */
+    private StringBuilder getStringBuilder(byte[] accumulate) {
+        BigInteger value = new BigInteger(1, accumulate);
+        int leadingZeros = 0;
+
+        for (byte b : accumulate) {
+            if (b == 0) {
+                leadingZeros++;
+            } else {
+                break;
+            }
+        }
+
+        final StringBuilder base58 = new StringBuilder();
+        while (value.signum() > 0) {
+            final BigInteger[] divRem = 
value.divideAndRemainder(BigInteger.valueOf(58));
+            base58.append((char) ENCODE_TABLE[divRem[1].intValue()]);
+            value = divRem[0];
+        }
+
+        for (int i = 0; i < leadingZeros; i++) {
+            base58.append('1');
+        }
+        return base58;
+    }
+
+    /**
+     * Converts Base58 encoded data to binary.
+     * <p>
+     * Uses BigInteger arithmetic to convert the Base58 string to binary data. 
Leading '1' characters
+     * in the Base58 encoding represent leading zero bytes in the binary data.
+     * </p>
+     *
+     * @param base58Data the Base58 encoded data
+     * @param context    the context for this decoding operation
+     * @throws IllegalArgumentException if the Base58 data contains invalid 
characters
+     */
+    private void convertFromBase58(byte[] base58Data, Context context) {
+        BigInteger value = BigInteger.ZERO;
+        int leadingOnes = 0;
+
+        for (byte b : base58Data) {
+            if (b == '1') {
+                leadingOnes++;
+            } else {
+                break;
+            }
+        }
+
+        final BigInteger base = BigInteger.valueOf(58);
+        BigInteger power = BigInteger.ONE;
+
+        for (int i = base58Data.length - 1; i >= leadingOnes; i--) {
+            final byte b = base58Data[i];
+            final int digit = b < DECODE_TABLE.length ? DECODE_TABLE[b] : -1;
+
+            if (digit < 0) {
+                throw new IllegalArgumentException("Invalid character in 
Base58 string: " + (char) b);
+            }
+
+            value = value.add(BigInteger.valueOf(digit).multiply(power));
+            power = power.multiply(base);
+        }
+
+        byte[] decoded = value.toByteArray();
+
+        if (decoded.length > 1 && decoded[0] == 0) {
+            final byte[] tmp = new byte[decoded.length - 1];
+            System.arraycopy(decoded, 1, tmp, 0, tmp.length);
+            decoded = tmp;
+        }
+
+        final byte[] result = new byte[leadingOnes + decoded.length];
+        System.arraycopy(decoded, 0, result, leadingOnes, decoded.length);
+
+        final byte[] buffer = ensureBufferSize(result.length, context);
+        System.arraycopy(result, 0, buffer, context.pos, result.length);
+        context.pos += result.length;
+    }
+
+    /**
+     * Returns whether or not the {@code octet} is in the Base58 alphabet.
+     *
+     * @param value The value to test.
+     * @return {@code true} if the value is defined in the Base58 alphabet 
{@code false} otherwise.
+     */
+    @Override
+    protected boolean isInAlphabet(byte value) {
+        return value >= 0 && value < DECODE_TABLE.length && 
DECODE_TABLE[value] != -1;
+    }
+
+    /**
+     * Builds {@link Base58} instances with custom configuration.
+     */
+    public static class Builder extends AbstractBuilder<Base58, 
Base58.Builder> {
+
+        /**
+         * Constructs a new Base58 builder.
+         */
+        public Builder() {
+            super(ENCODE_TABLE);
+            setDecodeTable(DECODE_TABLE);
+        }
+
+        /**
+         * Builds a new Base58 instance with the configured settings.
+
+         * @return a new Base58 codec
+         */
+        @Override
+        public Base58 get() {
+            return new Base58(this);
+        }
+
+        /**
+         * Creates a new Base58 codec instance.
+         *
+         * @return a new Base58 codec
+         */
+        @Override
+        public Base58.Builder setEncodeTable(final byte... encodeTable) {
+            super.setDecodeTableRaw(DECODE_TABLE);
+            return super.setEncodeTable(encodeTable);
+        }
+    }
+
+}
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
new file mode 100644
index 00000000..1c4115a0
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base58InputStream.java
@@ -0,0 +1,108 @@
+/*
+ * 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
+ *
+ *      https://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;
+
+/**
+ * Provides Base58 decoding in a streaming fashion (unlimited size). When 
encoding the default lineLength is 76 characters and the default lineEnding is 
CRLF,
+ * but these can be overridden by using the appropriate constructor.
+ * <p>
+ * The default behavior of the Base58InputStream is to DECODE, whereas the 
default behavior of the Base58OutputStream is to ENCODE, but this behavior can 
be
+ * overridden by using a different constructor.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character 
streams, it is hard-coded to only encode/decode character encodings which are
+ * compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, 
etc).
+ * </p>
+ * <p>
+ * You can set the decoding behavior when the input bytes contain leftover 
trailing bits that cannot be created by a valid encoding. These can be bits 
that are
+ * unused from the final character or entire characters. The default mode is 
lenient decoding.
+ * </p>
+ * <ul>
+ * <li>Lenient: Any trailing bits are composed into 8-bit bytes where 
possible. The remainder are discarded.</li>
+ * <li>Strict: The decoding will raise an {@link IllegalArgumentException} if 
trailing bits are not part of a valid encoding. Any unused bits from the final
+ * character must be zero. Impossible counts of entire final characters are 
not allowed.</li>
+ * </ul>
+ * <p>
+ * When strict decoding is enabled it is expected that the decoded bytes will 
be re-encoded to a byte array that matches the original, i.e. no changes occur 
on
+ * the final character. This requires that the input bytes use the same 
padding and alphabet as the encoder.
+ * </p>
+ *
+ * @see Base58
+ * @since 1.22.0
+ */
+public class Base58InputStream extends BaseNCodecInputStream<Base58, 
Base58InputStream, Base58InputStream.Builder> {
+
+    /**
+     * Builds instances of Base58InputStream.
+     */
+    public static class Builder extends 
BaseNCodecInputStream.AbstracBuilder<Base58InputStream, Base58, Builder> {
+
+        /**
+         * Constructs a new instance.
+         */
+        public Builder() {
+            // empty
+        }
+
+        @Override
+        public Base58InputStream get() {
+            return new Base58InputStream(this);
+        }
+
+        @Override
+        protected Base58 newBaseNCodec() {
+            return new Base58();
+        }
+    }
+
+    /**
+     * Constructs a new Builder.
+     *
+     * @return a new Builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private Base58InputStream(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Constructs a Base58InputStream such that all data read is 
Base58-decoded from the original provided InputStream.
+     *
+     * @param inputStream InputStream to wrap.
+     */
+    public Base58InputStream(final InputStream inputStream) {
+        super(builder().setInputStream(inputStream));
+    }
+
+    /**
+     * Constructs a Base58InputStream such that all data read is either 
Base58-encoded or Base58-decoded from the original provided InputStream.
+     *
+     * @param inputStream InputStream to wrap.
+     * @param encode    true if we should encode all data read from us, false 
if we should decode.
+     * @deprecated Use {@link #builder()} and {@link Builder}.
+     */
+    @Deprecated
+    public Base58InputStream(final InputStream inputStream, final boolean 
encode) {
+        super(builder().setInputStream(inputStream).setEncode(encode));
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
new file mode 100644
index 00000000..06a91e00
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base58OutputStream.java
@@ -0,0 +1,122 @@
+/*
+ * 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
+ *
+ *      https://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;
+
+/**
+ * Provides Base58 encoding in a streaming fashion (unlimited size). When 
encoding the default lineLength is 76 characters and the default lineEnding is 
CRLF,
+ * but these can be overridden by using the appropriate constructor.
+ * <p>
+ * The default behavior of the Base58OutputStream is to ENCODE, whereas the 
default behavior of the Base58InputStream is to DECODE. But this behavior can be
+ * overridden by using a different constructor.
+ * </p>
+ * <p>
+ * Since this class operates directly on byte streams, and not character 
streams, it is hard-coded to only encode/decode character encodings which are
+ * compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, 
etc).
+ * </p>
+ * <p>
+ * <strong>Note:</strong> It is mandatory to close the stream after the last 
byte has been written to it, otherwise the final padding will be omitted and the
+ * resulting data will be incomplete/inconsistent.
+ * </p>
+ * <p>
+ * You can set the decoding behavior when the input bytes contain leftover 
trailing bits that cannot be created by a valid encoding. These can be bits 
that are
+ * unused from the final character or entire characters. The default mode is 
lenient decoding.
+ * </p>
+ * <ul>
+ * <li>Lenient: Any trailing bits are composed into 8-bit bytes where 
possible. The remainder are discarded.</li>
+ * <li>Strict: The decoding will raise an {@link IllegalArgumentException} if 
trailing bits are not part of a valid encoding. Any unused bits from the final
+ * character must be zero. Impossible counts of entire final characters are 
not allowed.</li>
+ * </ul>
+ * <p>
+ * When strict decoding is enabled it is expected that the decoded bytes will 
be re-encoded to a byte array that matches the original, i.e. no changes occur 
on
+ * the final character. This requires that the input bytes use the same 
padding and alphabet as the encoder.
+ * </p>
+ *
+ * @see Base58
+ * @since 1.22.0
+ */
+public class Base58OutputStream extends BaseNCodecOutputStream<Base58, 
Base58OutputStream, Base58OutputStream.Builder> {
+
+    /**
+     * Builds instances of Base58OutputStream.
+     */
+    public static class Builder extends 
BaseNCodecOutputStream.AbstractBuilder<Base58OutputStream, Base58, Builder> {
+
+        /**
+         * Constructs a new instance.
+         */
+        public Builder() {
+            setEncode(true);
+        }
+
+        /**
+         * Builds a new Base58OutputStream instance with the configured 
settings.
+         *
+         * @return a new Base58OutputStream
+         */
+        @Override
+        public Base58OutputStream get() {
+            return new Base58OutputStream(this);
+        }
+
+        /**
+         * Creates a new Base58 codec instance.
+         *
+         * @return a new Base58 codec
+         */
+        @Override
+        protected Base58 newBaseNCodec() {
+            return new Base58();
+        }
+    }
+
+    /**
+     * Constructs a new Builder.
+     *
+     * @return a new Builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private Base58OutputStream(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Constructs a Base58OutputStream such that all data written is 
Base58-encoded to the original provided OutputStream.
+     *
+     * @param outputStream OutputStream to wrap.
+     */
+    public Base58OutputStream(final OutputStream outputStream) {
+        this(builder().setOutputStream(outputStream));
+    }
+
+    /**
+     * Constructs a Base58OutputStream such that all data written is either 
Base58-encoded or Base58-decoded to the original provided OutputStream.
+     *
+     * @param outputStream OutputStream to wrap.
+     * @param encode     true if we should encode all data written to us, 
false if we should decode.
+     * @deprecated Use {@link #builder()} and {@link Builder}.
+     */
+    @Deprecated
+    public Base58OutputStream(final OutputStream outputStream, final boolean 
encode) {
+        super(builder().setOutputStream(outputStream).setEncode(encode));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base58InputStreamTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base58InputStreamTest.java
new file mode 100644
index 00000000..aa01c08f
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base58InputStreamTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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
+ *
+ *      https://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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Base58InputStream}.
+ */
+class Base58InputStreamTest {
+
+    private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };
+
+    private static final byte[] LF = { (byte) '\n' };
+
+    private static final String STRING_FIXTURE = "Hello World";
+
+    @Test
+    void testAvailable() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            final int initialAvailable = b58stream.available();
+            assertTrue(initialAvailable > 0, "Initial available should be 
greater than 0");
+            assertEquals(3, b58stream.skip(10), "Skip should return 3 (decoded 
bytes)");
+            assertEquals(0, b58stream.available());
+            assertEquals(-1, b58stream.read());
+            assertEquals(-1, b58stream.read());
+        }
+    }
+
+    private void testBase58EmptyInputStream(final int chuckSize) throws 
Exception {
+        final byte[] emptyEncoded = {};
+        final byte[] emptyDecoded = {};
+        testByChunk(emptyEncoded, emptyDecoded, chuckSize, CRLF);
+        testByteByByte(emptyEncoded, emptyDecoded, chuckSize, CRLF);
+    }
+
+    /**
+     * Tests the Base58InputStream implementation against empty input.
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testBase58EmptyInputStreamMimeChuckSize() throws Exception {
+        testBase58EmptyInputStream(BaseNCodec.MIME_CHUNK_SIZE);
+    }
+
+    /**
+     * Tests the Base58InputStream implementation against empty input.
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testBase58EmptyInputStreamPemChuckSize() throws Exception {
+        testBase58EmptyInputStream(BaseNCodec.PEM_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase58InputStreamByChunk() throws Exception {
+        // Hello World test.
+        byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        byte[] encoded = new Base58().encode(decoded);
+        testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
+
+        // test random data of sizes 0 through 150
+        final BaseNCodec codec = new Base58();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByChunk(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBase58InputStreamByteByByte() throws Exception {
+        // Hello World test.
+        byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        byte[] encoded = new Base58().encode(decoded);
+        testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
+
+        // test random data of sizes 0 through 150
+        final BaseNCodec codec = new Base58();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByteByByte(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBuilder() {
+        assertNotNull(Base58InputStream.builder().getBaseNCodec());
+    }
+
+    /**
+     * 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 
Base58InputStream wraps itself in encode and decode mode over and over
+     * again.
+     *
+     * @param encoded
+     *            base58 encoded data
+     * @param decoded
+     *            the data from above, but decoded
+     * @param chunkSize
+     *            chunk size (line-length) of the base58 encoded data.
+     * @param separator
+     *            Line separator in the base58 encoded data.
+     * @throws Exception
+     *             Usually signifies a bug in the Base58 commons-codec 
implementation.
+     */
+    private void testByChunk(final byte[] encoded, final byte[] decoded, final 
int chunkSize, final byte[] separator) throws Exception {
+        try (InputStream in = new Base58InputStream(new 
ByteArrayInputStream(decoded), true)) {
+            final byte[] output = BaseNTestData.streamToBytes(in);
+            assertEquals(-1, in.read(), "EOF");
+            assertEquals(-1, in.read(), "Still EOF");
+            assertArrayEquals(encoded, output, "Streaming base58 encode");
+        }
+        try (InputStream in = new Base58InputStream(new 
ByteArrayInputStream(encoded))) {
+            final byte[] output = BaseNTestData.streamToBytes(in);
+
+            assertEquals(-1, in.read(), "EOF");
+            assertEquals(-1, in.read(), "Still EOF");
+            assertArrayEquals(decoded, output, "Streaming base58 decode");
+        }
+        InputStream in = new ByteArrayInputStream(decoded);
+        for (int i = 0; i < 10; i++) {
+            in = new Base58InputStream(in, true);
+            in = new Base58InputStream(in, false);
+        }
+        final byte[] output = BaseNTestData.streamToBytes(in);
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming base58 wrap-wrap-wrap!");
+        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 
Base58InputStream wraps itself in encode and decode mode over and over
+     * again.
+     *
+     * @param encoded
+     *            base58 encoded data
+     * @param decoded
+     *            the data from above, but decoded
+     * @param chunkSize
+     *            chunk size (line-length) of the base58 encoded data.
+     * @param separator
+     *            Line separator in the base58 encoded data.
+     * @throws Exception
+     *             Usually signifies a bug in the Base58 commons-codec 
implementation.
+     */
+    private void testByteByByte(final byte[] encoded, final byte[] decoded, 
final int chunkSize, final byte[] separator) throws Exception {
+        InputStream in;
+        in = new Base58InputStream(new ByteArrayInputStream(decoded), true);
+        byte[] output = BaseNTestData.streamToBytes(in);
+
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(encoded, output, "Streaming base58 encode");
+
+        in.close();
+        in = new Base58InputStream(new ByteArrayInputStream(encoded));
+        output = BaseNTestData.streamToBytes(in);
+
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming base58 decode");
+
+        in.close();
+        in = new ByteArrayInputStream(decoded);
+        for (int i = 0; i < 10; i++) {
+            in = new Base58InputStream(in, true);
+            in = new Base58InputStream(in, false);
+        }
+        output = BaseNTestData.streamToBytes(in);
+
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming base58 wrap-wrap-wrap!");
+    }
+
+    /**
+     * Tests markSupported.
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testMarkSupported() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
+        try (Base58InputStream in = new Base58InputStream(bin, true)) {
+            // Always returns false for now.
+            assertFalse(in.markSupported(), "Base58InputStream.markSupported() 
is false");
+        }
+    }
+
+    /**
+     * Tests read returning 0
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testRead0() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final byte[] buf = new byte[1024];
+        int bytesRead = 0;
+        final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
+        try (Base58InputStream in = new Base58InputStream(bin, true)) {
+            bytesRead = in.read(buf, 0, 0);
+            assertEquals(0, bytesRead, "Base58InputStream.read(buf, 0, 0) 
returns 0");
+        }
+    }
+
+    /**
+     * Tests read with null.
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testReadNull() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
+        try (Base58InputStream in = new Base58InputStream(bin, true)) {
+            assertThrows(NullPointerException.class, () -> in.read(null, 0, 
0));
+        }
+    }
+
+    /**
+     * Tests read throwing IndexOutOfBoundsException
+     *
+     * @throws Exception
+     *             for some failure scenarios.
+     */
+    @Test
+    void testReadOutOfBounds() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final byte[] buf = new byte[1024];
+        final ByteArrayInputStream bin = new ByteArrayInputStream(decoded);
+        try (Base58InputStream in = new Base58InputStream(bin, true)) {
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
-1, 0), "Base58InputStream.read(buf, -1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
0, -1), "Base58InputStream.read(buf, 0, -1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
buf.length + 1, 0), "Base58InputStream.read(buf, buf.length + 1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
buf.length - 1, 2), "Base58InputStream.read(buf, buf.length - 1, 2)");
+        }
+    }
+
+    /**
+     * Tests skipping number of characters larger than the internal buffer.
+     *
+     * @throws Throwable
+     *             for some failure scenarios.
+     */
+    @Test
+    void testSkipBig() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            assertEquals(3, b58stream.skip(1024));
+            // End of stream reached
+            assertEquals(-1, b58stream.read());
+            assertEquals(-1, b58stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping as a noop
+     *
+     * @throws Throwable
+     *             for some failure scenarios.
+     */
+    @Test
+    void testSkipNone() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            final byte[] actualBytes = new byte[3];
+            assertEquals(0, b58stream.skip(0));
+            b58stream.read(actualBytes, 0, actualBytes.length);
+            assertArrayEquals(actualBytes, new byte[] { 102, 111, 111 });
+            // End of stream reached
+            assertEquals(-1, b58stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping past the end of a stream.
+     *
+     * @throws Throwable
+     *             for some failure scenarios.
+     */
+    @Test
+    void testSkipPastEnd() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            // skip correctly decoded characters
+            assertEquals(3, b58stream.skip(10));
+            // End of stream reached
+            assertEquals(-1, b58stream.read());
+            assertEquals(-1, b58stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping to the end of a stream.
+     *
+     * @throws Throwable
+     *             for some failure scenarios.
+     */
+    @Test
+    void testSkipToEnd() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            // skip correctly decoded characters
+            assertEquals(3, b58stream.skip(3));
+            assertEquals(-1, b58stream.read());
+        }
+    }
+
+    /**
+     * Tests if negative arguments to skip are handled correctly.
+     *
+     * @throws Throwable
+     *             for some failure scenarios.
+     */
+    @Test
+    void testSkipWrongArgument() throws Throwable {
+        final String encoded = new String(new 
Base58().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base58InputStream b58stream = new Base58InputStream(ins)) {
+            assertThrows(IllegalArgumentException.class, () -> 
b58stream.skip(-1));
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base58OutputStreamTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base58OutputStreamTest.java
new file mode 100644
index 00000000..3e57569e
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base58OutputStreamTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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
+ *
+ *      https://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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Base58OutputStream}.
+ */
+class Base58OutputStreamTest {
+
+    private static final byte[] CR_LF = {(byte) '\r', (byte) '\n'};
+
+    private static final byte[] LF = {(byte) '\n'};
+
+    private void testBase58EmptyOutputStream(final int chunkSize) throws 
Exception {
+        final byte[] emptyEncoded = {};
+        final byte[] emptyDecoded = {};
+        testByteByByte(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
+        testByChunk(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
+    }
+
+    @Test
+    void testBase58EmptyOutputStreamMimeChunkSize() throws Exception {
+        testBase58EmptyOutputStream(BaseNCodec.MIME_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase58EmptyOutputStreamPemChunkSize() throws Exception {
+        testBase58EmptyOutputStream(BaseNCodec.PEM_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase58OutputStreamByChunk() throws Exception {
+        byte[] decoded = StringUtils.getBytesUtf8("Hello World");
+        byte[] encoded = new Base58().encode(decoded);
+        testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CR_LF);
+
+        final BaseNCodec codec = new Base58();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByChunk(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBase58OutputStreamByteByByte() throws Exception {
+        byte[] decoded = StringUtils.getBytesUtf8("Hello World");
+        byte[] encoded = new Base58().encode(decoded);
+        testByteByByte(encoded, decoded, 76, CR_LF);
+
+        final BaseNCodec codec = new Base58();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByteByByte(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBuilder() {
+        assertNotNull(Base58OutputStream.builder().getBaseNCodec());
+    }
+
+    private void testByChunk(final byte[] encoded, final byte[] decoded, final 
int chunkSize, final byte[] separator) throws Exception {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = new Base58OutputStream(byteOut, true)) {
+            out.write(decoded);
+        }
+        byte[] output = byteOut.toByteArray();
+        assertArrayEquals(encoded, output, "Streaming chunked Base58 encode");
+
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = new Base58OutputStream(byteOut, false)) {
+            out.write(encoded);
+        }
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming chunked Base58 decode");
+
+        byteOut = new ByteArrayOutputStream();
+        OutputStream out = byteOut;
+        for (int i = 0; i < 10; i++) {
+            out = new Base58OutputStream(out, false);
+            out = new Base58OutputStream(out, true);
+        }
+        out.write(decoded);
+        out.close();
+        output = byteOut.toByteArray();
+
+        assertArrayEquals(decoded, byteOut.toByteArray(), "Streaming chunked 
Base58 wrap-wrap-wrap!");
+    }
+
+    private void testByteByByte(final byte[] encoded, final byte[] decoded, 
final int chunkSize, final byte[] separator) throws Exception {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = new Base58OutputStream(byteOut, true)) {
+            for (final byte element : decoded) {
+                out.write(element);
+            }
+        }
+        byte[] output = byteOut.toByteArray();
+        assertArrayEquals(encoded, output, "Streaming byte-by-byte Base58 
encode");
+
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = new Base58OutputStream(byteOut, false)) {
+            for (final byte element : encoded) {
+                out.write(element);
+            }
+        }
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming byte-by-byte Base58 
decode");
+
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = new Base58OutputStream(byteOut, false)) {
+            for (final byte element : encoded) {
+                out.write(element);
+                out.flush();
+            }
+        }
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming byte-by-byte flush() 
Base58 decode");
+
+        byteOut = new ByteArrayOutputStream();
+        OutputStream out = byteOut;
+        for (int i = 0; i < 10; i++) {
+            out = new Base58OutputStream(out, false);
+            out = new Base58OutputStream(out, true);
+        }
+        for (final byte element : decoded) {
+            out.write(element);
+        }
+        out.close();
+        output = byteOut.toByteArray();
+
+        assertArrayEquals(decoded, output, "Streaming byte-by-byte Base58 
wrap-wrap-wrap!");
+    }
+
+    @Test
+    void testWriteOutOfBounds() throws Exception {
+        final byte[] buf = new byte[1024];
+        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        try (Base58OutputStream out = new Base58OutputStream(bout)) {
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
-1, 1), "Base58OutputStream.write(buf, -1, 1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
1, -1), "Base58OutputStream.write(buf, 1, -1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
buf.length + 1, 0), "Base58OutputStream.write(buf, buf.length + 1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
buf.length - 1, 2), "Base58OutputStream.write(buf, buf.length - 1, 2)");
+        }
+    }
+
+    @Test
+    void testWriteToNullCoverage() throws Exception {
+        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        try (Base58OutputStream out = new Base58OutputStream(bout)) {
+            assertThrows(NullPointerException.class, () -> out.write(null, 0, 
0));
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/codec/binary/Base58Test.java 
b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
new file mode 100644
index 00000000..14975e5d
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
@@ -0,0 +1,262 @@
+/*
+ * 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
+ *
+ *      https://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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Random;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.EncoderException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Base58}.
+ */
+public class Base58Test {
+
+    private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
+
+    private final Random random = new Random();
+
+    @Test
+    void testBase58() {
+        final String content = "Hello World";
+        final byte[] encodedBytes = new 
Base58().encode(StringUtils.getBytesUtf8(content));
+        final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
+        assertEquals("JxF12TrwUP45BMd", encodedContent, "encoding hello 
world");
+
+        final byte[] decodedBytes = new Base58().decode(encodedBytes);
+        final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
+        assertEquals(content, decodedContent, "decoding hello world");
+    }
+
+    @Test
+    void testEmptyBase58() {
+        byte[] empty = {};
+        byte[] result = new Base58().encode(empty);
+        assertEquals(0, result.length, "empty Base58 encode");
+        assertNull(new Base58().encode(null), "empty Base58 encode");
+
+        empty = new byte[0];
+        result = new Base58().decode(empty);
+        assertEquals(0, result.length, "empty Base58 decode");
+        assertNull(new Base58().decode((byte[]) null), "empty Base58 decode");
+    }
+
+    @Test
+    void testEncodeDecodeSmall() {
+        for (int i = 0; i < 12; i++) {
+            final byte[] data = new byte[i];
+            random.nextBytes(data);
+            final byte[] enc = new Base58().encode(data);
+            final byte[] dec = new Base58().decode(enc);
+            assertArrayEquals(data, dec);
+        }
+    }
+
+    @Test
+    void testEncodeDecodeRandom() {
+        for (int i = 1; i < 5; i++) {
+            final byte[] data = new byte[random.nextInt(10000) + 1];
+            random.nextBytes(data);
+            final byte[] enc = new Base58().encode(data);
+            final byte[] dec = new Base58().decode(enc);
+            assertArrayEquals(data, dec);
+        }
+    }
+
+    @Test
+    void testIsInAlphabet() {
+        final Base58 base58 = new Base58();
+
+        // Valid characters
+        for (char c = '1'; c <= '9'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+        for (char c = 'A'; c <= 'H'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+        for (char c = 'J'; c <= 'N'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+        for (char c = 'P'; c <= 'Z'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+        for (char c = 'a'; c <= 'k'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+        for (char c = 'm'; c <= 'z'; c++) {
+            assertTrue(base58.isInAlphabet((byte) c), "char " + c);
+        }
+
+        // Invalid characters - excluded from Base58
+        assertFalse(base58.isInAlphabet((byte) '0'), "char 0");
+        assertFalse(base58.isInAlphabet((byte) 'O'), "char O");
+        assertFalse(base58.isInAlphabet((byte) 'I'), "char I");
+        assertFalse(base58.isInAlphabet((byte) 'l'), "char l");
+
+        // Out of bounds
+        assertFalse(base58.isInAlphabet((byte) -1));
+        assertFalse(base58.isInAlphabet((byte) 0));
+        assertFalse(base58.isInAlphabet((byte) 128));
+        assertFalse(base58.isInAlphabet((byte) 255));
+    }
+
+    @Test
+    void testObjectDecodeWithInvalidParameter() {
+        assertThrows(DecoderException.class, () -> new 
Base58().decode(Integer.valueOf(5)));
+    }
+
+    @Test
+    void testObjectDecodeWithValidParameter() throws Exception {
+        final String original = "Hello World!";
+        final Object o = new Base58().encode(original.getBytes(CHARSET_UTF8));
+
+        final Base58 base58 = new Base58();
+        final Object oDecoded = base58.decode(o);
+        final byte[] baDecoded = (byte[]) oDecoded;
+        final String dest = new String(baDecoded);
+
+        assertEquals(original, dest, "dest string does not equal original");
+    }
+
+    @Test
+    void testObjectEncodeWithInvalidParameter() {
+        assertThrows(EncoderException.class, () -> new 
Base58().encode("Yadayadayada"));
+    }
+
+    @Test
+    void testObjectEncodeWithValidParameter() throws Exception {
+        final String original = "Hello World!";
+        final Object origObj = original.getBytes(CHARSET_UTF8);
+
+        final Object oEncoded = new Base58().encode(origObj);
+        final byte[] bArray = new Base58().decode((byte[]) oEncoded);
+        final String dest = new String(bArray);
+
+        assertEquals(original, dest, "dest string does not equal original");
+    }
+
+    @Test
+    void testLeadingZeros() {
+        // Test that leading zero bytes are encoded as '1' characters
+        final byte[] input = new byte[]{0, 0, 1, 2, 3};
+        final byte[] encoded = new Base58().encode(input);
+        final String encodedStr = new String(encoded);
+
+        // Should start with "11" (two leading ones for two leading zeros)
+        assertTrue(encodedStr.startsWith("11"), "Leading zeros should encode 
as '1' characters");
+
+        // Decode should restore the leading zeros
+        final byte[] decoded = new Base58().decode(encoded);
+        assertArrayEquals(input, decoded, "Decoded should match original 
including leading zeros");
+    }
+
+    @Test
+    void testSingleBytes() {
+        // Test encoding of single bytes
+        for (int i = 1; i <= 255; i++) {
+            final byte[] data = new byte[]{(byte) i};
+            final byte[] enc = new Base58().encode(data);
+            final byte[] dec = new Base58().decode(enc);
+            assertArrayEquals(data, dec, "Failed for byte value: " + i);
+        }
+    }
+
+    @Test
+    void testInvalidCharacters() {
+        // Test decoding with invalid characters (those not in Base58 alphabet)
+        final byte[] invalidChars = "0OIl".getBytes(CHARSET_UTF8); // All 
excluded from Base58
+        assertThrows(IllegalArgumentException.class, () -> new 
Base58().decode(invalidChars));
+    }
+
+    @Test
+    void testRoundTrip() {
+        final String[] testStrings = {
+                "",
+                "a",
+                "ab",
+                "abc",
+                "abcd",
+                "abcde",
+                "abcdef",
+                "Hello World",
+                "The quick brown fox jumps over the lazy dog",
+                "1234567890",
+                "!@#$%^&*()"
+        };
+
+        for (final String test : testStrings) {
+            final byte[] input = test.getBytes(CHARSET_UTF8);
+            final byte[] encoded = new Base58().encode(input);
+            final byte[] decoded = new Base58().decode(encoded);
+            assertArrayEquals(input, decoded, "Round trip failed for: " + 
test);
+        }
+    }
+
+    @Test
+    void testHexEncoding() {
+        final String hexString = "48656c6c6f20576f726c6421";
+        final byte[] encoded = new 
Base58().encode(StringUtils.getBytesUtf8(hexString));
+        final byte[] decoded = new 
Base58().decode(StringUtils.newStringUtf8(encoded));
+
+        assertEquals("5m7UdtXCfQxGvX2K9dLrkNs7AFMS98qn8", 
StringUtils.newStringUtf8(encoded), "Hex encoding failed");
+        assertEquals(hexString, StringUtils.newStringUtf8(decoded), "Hex 
decoding failed");
+    }
+
+    @Test
+    void testTestVectors() {
+        final String content = "Hello World!";
+        final String content1 = "The quick brown fox jumps over the lazy dog.";
+        final long content2 = 0x0000287fb4cdL; // Use long to preserve the 
full 48-bit value
+
+        final byte[] encodedBytes = new 
Base58().encode(StringUtils.getBytesUtf8(content));
+        final byte[] encodedBytes1 = new 
Base58().encode(StringUtils.getBytesUtf8(content1));
+
+        final byte[] content2Bytes = 
ByteBuffer.allocate(8).putLong(content2).array();
+        final byte[] content2Trimmed = new byte[6];
+        System.arraycopy(content2Bytes, 2, content2Trimmed, 0, 6);
+        final byte[] encodedBytes2 = new Base58().encode(content2Trimmed);
+
+        final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
+        final String encodedContent1 = 
StringUtils.newStringUtf8(encodedBytes1);
+        final String encodedContent2 = 
StringUtils.newStringUtf8(encodedBytes2);
+
+        assertEquals("2NEpo7TZRRrLZSi2U", encodedContent, "encoding hello 
world");
+        
assertEquals("USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", 
encodedContent1);
+        assertEquals("11233QC4", encodedContent2, "encoding 0x0000287fb4cd");
+
+        final byte[] decodedBytes = new Base58().decode(encodedBytes);
+        final byte[] decodedBytes1 = new Base58().decode(encodedBytes1);
+        final byte[] decodedBytes2 = new Base58().decode(encodedBytes2);
+        final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
+        final String decodedContent1 = 
StringUtils.newStringUtf8(decodedBytes1);
+        assertEquals(content, decodedContent, "decoding hello world");
+        assertEquals(content1, decodedContent1);
+        assertArrayEquals(content2Trimmed, decodedBytes2, "decoding 
0x0000287fb4cd");
+    }
+}

Reply via email to