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. + *

+ * + *

+ * 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"); + } +}