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 0000000000..f94938b246
--- /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.
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * This class is thread-safe for read operations but the Context object used during encoding/decoding should
+ * not be shared between threads.
+ *
+ *
+ * The Base58 alphabet is: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
+ * (excludes: 0, I, O, l)
+ *
+ *
+ * @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 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.
+ *
+ * This implementation accumulates data internally. When length < 0 (EOF), the accumulated
+ * data is converted from Base58 to binary.
+ *
+ *
+ * @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.
+ *
+ * This implementation accumulates data internally. When length < 0 (EOF), the accumulated
+ * data is converted to Base58.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ *
+ * @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 {
+
+ /**
+ * 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 0000000000..1c4115a07b
--- /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.
+ *
+ * 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.
+ *
+ *
+ * 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).
+ *
+ *
+ * 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.
+ *
+ *
+ * - Lenient: Any trailing bits are composed into 8-bit bytes where possible. The remainder are discarded.
+ * - 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * @see Base58
+ * @since 1.22.0
+ */
+public class Base58InputStream extends BaseNCodecInputStream {
+
+ /**
+ * Builds instances of Base58InputStream.
+ */
+ public static class Builder extends BaseNCodecInputStream.AbstracBuilder {
+
+ /**
+ * 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 0000000000..06a91e00e5
--- /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.
+ *
+ * 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.
+ *
+ *
+ * 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).
+ *
+ *
+ * Note: 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * - Lenient: Any trailing bits are composed into 8-bit bytes where possible. The remainder are discarded.
+ * - 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * @see Base58
+ * @since 1.22.0
+ */
+public class Base58OutputStream extends BaseNCodecOutputStream {
+
+ /**
+ * Builds instances of Base58OutputStream.
+ */
+ public static class Builder extends BaseNCodecOutputStream.AbstractBuilder {
+
+ /**
+ * 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 0000000000..aa01c08f1a
--- /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
+ *
+ * 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
+ *
+ * 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 0000000000..3e57569ed3
--- /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 0000000000..14975e5dca
--- /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");
+ }
+}