This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git
The following commit(s) were added to refs/heads/master by this push:
new 98986558 [CODEC-340] Fix Base58 custom alphabet handling (#437)
98986558 is described below
commit 98986558dc1c0e2013b76bea92d75bde80568a00
Author: OldTruckDriver <[email protected]>
AuthorDate: Thu Jun 18 12:16:35 2026 +1000
[CODEC-340] Fix Base58 custom alphabet handling (#437)
Derive a matching decode table from custom Base58 encode tables and use the
configured tables when encoding, decoding, and checking the alphabet.
Handle leading zero bytes with the first entry of the configured alphabet
instead of hard-coding '1'.
Reviewed-by: OpenAI Codex
Reviewed-by: Anthropic Claude Code
Co-authored-by: Gary Gregory <[email protected]>
---
src/changes/changes.xml | 1 +
.../org/apache/commons/codec/binary/Base58.java | 92 ++++++++++++++++------
.../apache/commons/codec/binary/Base58Test.java | 53 +++++++++++++
3 files changed, 123 insertions(+), 23 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4ec269cc..ba07a02d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -45,6 +45,7 @@ The <action> type attribute can be add,update,fix,remove.
<body>
<release version="1.22.1" date="YYYY-MM-DD" description="This is a feature
and maintenance release. Java 8 or later is required.">
<!-- FIX -->
+ <action type="fix" issue="CODEC-340" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base58.Builder.setEncodeTable(byte...) is ignored when encoding
and decoding.</action>
<action type="fix" issue="CODEC-342" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base32.Builder.setEncodeTable(byte...) can create a codec that
cannot decode its own output.</action>
<action type="fix" issue="CODEC-343" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base32.Builder.setHexDecodeTable(boolean) sets the encode table
to a decode lookup table.</action>
<action type="fix" issue="CODEC-341" dev="ggregory" due-to="Ruiqi Dong,
Gary Gregory">Base16.Builder.setEncodeTable(byte...) can create a codec that
cannot decode its own output.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base58.java
b/src/main/java/org/apache/commons/codec/binary/Base58.java
index 4987cba4..b07e950d 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base58.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base58.java
@@ -18,7 +18,7 @@
package org.apache.commons.codec.binary;
import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
/**
* Provides Base58 encoding and decoding as commonly used in cryptocurrency
and blockchain applications.
@@ -75,18 +75,23 @@ public class Base58 extends BaseNCodec {
}
/**
- * Creates a new Base58 codec instance.
+ * Sets the encode table and derives the matching decode table.
*
- * @return a new Base58 codec.
+ * @param encodeTable the encode table with exactly 58 unique entries,
null resets to the default.
+ * @return {@code this} instance.
+ * @throws IllegalArgumentException if the encode table does not
contain exactly 58 unique entries.
*/
@Override
public Base58.Builder setEncodeTable(final byte... encodeTable) {
- super.setDecodeTableRaw(DECODE_TABLE);
+ super.setDecodeTableRaw(toDecodeTable(encodeTable));
return super.setEncodeTable(encodeTable);
}
}
private static final BigInteger BASE = BigInteger.valueOf(58);
+ private static final int DECODING_TABLE_LENGTH = 256;
+ private static final int ENCODING_TABLE_LENGTH = 58;
+
private static final byte[] EMPTY = {};
/**
@@ -138,6 +143,43 @@ public class Base58 extends BaseNCodec {
return new Builder();
}
+ /**
+ * Calculates a decode table for a given encode table.
+ *
+ * @param encodeTable that is used to determine decode lookup table.
+ * @return A new decode table.
+ * @throws IllegalArgumentException if the encode table does not contain
exactly 58 unique entries.
+ */
+ private static byte[] calculateDecodeTable(final byte[] encodeTable) {
+ if (encodeTable.length != ENCODING_TABLE_LENGTH) {
+ throw new IllegalArgumentException("encodeTable must have exactly
58 entries.");
+ }
+ final byte[] decodeTable = new byte[DECODING_TABLE_LENGTH];
+ Arrays.fill(decodeTable, (byte) -1);
+ for (int i = 0; i < encodeTable.length; i++) {
+ final int encodedByte = encodeTable[i] & 0xff;
+ if (decodeTable[encodedByte] != -1) {
+ throw new IllegalArgumentException("encodeTable must not
contain duplicate entries.");
+ }
+ decodeTable[encodedByte] = (byte) i;
+ }
+ return decodeTable;
+ }
+
+ /**
+ * Gets the decode table that matches the given encode table.
+ *
+ * @param encodeTable that is used to determine decode lookup table.
+ * @return the matching decode table.
+ */
+ private static byte[] toDecodeTable(final byte[] encodeTable) {
+ final byte[] table = encodeTable != null ? encodeTable : ENCODE_TABLE;
+ if (Arrays.equals(table, ENCODE_TABLE)) {
+ return DECODE_TABLE;
+ }
+ return calculateDecodeTable(table);
+ }
+
/**
* Constructs a Base58 codec used for encoding and decoding.
*/
@@ -157,8 +199,8 @@ public class Base58 extends BaseNCodec {
/**
* Converts Base58 encoded data to binary.
* <p>
- * Uses BigInteger arithmetic to convert the Base58 string to binary data.
Leading '1' characters in the Base58 encoding represent leading zero bytes in
the
- * binary data.
+ * Uses BigInteger arithmetic to convert the Base58 string to binary data.
Leading characters that match the first Base58 alphabet entry represent leading
+ * zero bytes in the binary data.
* </p>
*
* @param base58 the Base58 encoded data.
@@ -167,17 +209,18 @@ public class Base58 extends BaseNCodec {
*/
private void convertFromBase58(final byte[] base58, final Context context)
{
BigInteger value = BigInteger.ZERO;
- int leadingOnes = 0;
+ int leadingZeros = 0;
+ final int zero = encodeTable[0] & 0xff;
for (final byte b : base58) {
- if (b != '1') {
+ if ((b & 0xff) != zero) {
break;
}
- leadingOnes++;
+ leadingZeros++;
}
BigInteger power = BigInteger.ONE;
- for (int i = base58.length - 1; i >= leadingOnes; i--) {
- final byte b = base58[i];
- final int digit = b < DECODE_TABLE.length ? DECODE_TABLE[b] : -1;
+ for (int i = base58.length - 1; i >= leadingZeros; i--) {
+ final int b = base58[i] & 0xff;
+ final int digit = b < decodeTable.length ? decodeTable[b] : -1;
if (digit < 0) {
throw new IllegalArgumentException(String.format("Invalid
character in Base58 string: 0x%02x", b));
}
@@ -190,8 +233,8 @@ public class Base58 extends BaseNCodec {
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[] result = new byte[leadingZeros + decoded.length];
+ System.arraycopy(decoded, 0, result, leadingZeros, decoded.length);
final byte[] buffer = ensureBufferSize(result.length, context);
System.arraycopy(result, 0, buffer, context.pos, result.length);
context.pos += result.length;
@@ -200,8 +243,8 @@ public class Base58 extends BaseNCodec {
/**
* Converts accumulated binary data to Base58 encoding.
* <p>
- * Uses BigInteger arithmetic to convert the binary data to Base58.
Leading zeros in the binary data are represented as '1' characters in the Base58
- * encoding.
+ * Uses BigInteger arithmetic to convert the binary data to Base58.
Leading zeros in the binary data are represented as the first character in the
Base58
+ * alphabet.
* </p>
*
* @param accumulate the binary data to encode.
@@ -210,8 +253,10 @@ public class Base58 extends BaseNCodec {
*/
private byte[] convertToBase58(final byte[] accumulate, final Context
context) {
final StringBuilder base58 = getStringBuilder(accumulate);
- final String encoded = base58.reverse().toString();
- final byte[] encodedBytes = encoded.getBytes(StandardCharsets.UTF_8);
+ final byte[] encodedBytes = new byte[base58.length()];
+ for (int i = 0; i < encodedBytes.length; i++) {
+ encodedBytes[i] = (byte) base58.charAt(encodedBytes.length - 1 -
i);
+ }
final byte[] buffer = ensureBufferSize(encodedBytes.length, context);
System.arraycopy(encodedBytes, 0, buffer, context.pos,
encodedBytes.length);
context.pos += encodedBytes.length;
@@ -285,8 +330,8 @@ public class Base58 extends BaseNCodec {
/**
* Builds the Base58 string representation of the given binary data.
* <p>
- * Converts binary data to a BigInteger and divides by 58 repeatedly to
get the Base58 digits. Handles leading zeros by counting them and appending '1'
for
- * each leading zero byte.
+ * Converts binary data to a BigInteger and divides by 58 repeatedly to
get the Base58 digits. Handles leading zeros by counting them and appending the
first
+ * character in the Base58 alphabet for each leading zero byte.
* </p>
*
* @param accumulate the binary data to convert.
@@ -304,11 +349,11 @@ public class Base58 extends BaseNCodec {
final StringBuilder base58 = new StringBuilder();
while (value.signum() > 0) {
final BigInteger[] divRem = value.divideAndRemainder(BASE);
- base58.append((char) ENCODE_TABLE[divRem[1].intValue()]);
+ base58.append((char) (encodeTable[divRem[1].intValue()] & 0xff));
value = divRem[0];
}
for (int i = 0; i < leadingZeros; i++) {
- base58.append('1');
+ base58.append((char) (encodeTable[0] & 0xff));
}
return base58;
}
@@ -321,6 +366,7 @@ public class Base58 extends BaseNCodec {
*/
@Override
protected boolean isInAlphabet(final byte value) {
- return isInAlphabet(value, DECODE_TABLE);
+ final int octet = value & 0xff;
+ return octet < decodeTable.length && decodeTable[octet] != -1;
}
}
diff --git a/src/test/java/org/apache/commons/codec/binary/Base58Test.java
b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
index c5772e52..5a0ffb5e 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base58Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base58Test.java
@@ -46,6 +46,8 @@ public class Base58Test {
private static final int BOUND = 10_000;
+ private static final String DEFAULT_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+
private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
private static void assertArrayEqualsAt(final byte[] data, final byte[]
dec, final int i) {
@@ -53,6 +55,18 @@ public class Base58Test {
assertArrayEquals(data, dec, () -> String.format("Failed for length
%,d: %s", counter.get(), Arrays.toString(data)));
}
+ private static byte[] newEncodeTable() {
+ return DEFAULT_ALPHABET.getBytes(StandardCharsets.US_ASCII);
+ }
+
+ private static byte[] newSwappedEncodeTable() {
+ final byte[] encodeTable = newEncodeTable();
+ final byte tmp = encodeTable[0];
+ encodeTable[0] = encodeTable[1];
+ encodeTable[1] = tmp;
+ return encodeTable;
+ }
+
private final Random random = new Random();
@Test
@@ -66,6 +80,45 @@ public class Base58Test {
assertEquals(content, decodedContent, "decoding hello world");
}
+ @Test
+ void testBuilderCustomEncodeTableAffectsEncodeAndDecode() {
+ final Base58 base58 =
Base58.builder().setEncodeTable(newSwappedEncodeTable()).get();
+ assertEquals("1", new String(base58.encode(new byte[] { 1 }),
StandardCharsets.US_ASCII));
+ assertArrayEquals(new byte[] { 1 },
base58.decode("1".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ @Test
+ void testBuilderCustomEncodeTableAffectsIsInAlphabet() {
+ final byte[] encodeTable = newEncodeTable();
+ encodeTable[0] = '0';
+ final Base58 base58 =
Base58.builder().setEncodeTable(encodeTable).get();
+ assertTrue(base58.isInAlphabet((byte) '0'));
+ assertFalse(base58.isInAlphabet((byte) '1'));
+ assertEquals("0", new String(base58.encode(new byte[] { 0 }),
StandardCharsets.US_ASCII));
+ assertArrayEquals(new byte[] { 0 },
base58.decode("0".getBytes(StandardCharsets.US_ASCII)));
+ }
+
+ @Test
+ void testBuilderCustomEncodeTableAffectsLeadingZeros() {
+ final Base58 base58 =
Base58.builder().setEncodeTable(newSwappedEncodeTable()).get();
+ final byte[] data = { 0, 0, 1 };
+ final byte[] encoded = base58.encode(data);
+ assertEquals("221", new String(encoded, StandardCharsets.US_ASCII));
+ assertArrayEquals(data, base58.decode(encoded));
+ }
+
+ @Test
+ void testBuilderCustomEncodeTableRejectsDuplicateEntries() {
+ final byte[] encodeTable = newEncodeTable();
+ encodeTable[1] = encodeTable[0];
+ assertThrows(IllegalArgumentException.class, () ->
Base58.builder().setEncodeTable(encodeTable));
+ }
+
+ @Test
+ void testBuilderCustomEncodeTableRejectsInvalidLength() {
+ assertThrows(IllegalArgumentException.class, () ->
Base58.builder().setEncodeTable(Arrays.copyOf(newEncodeTable(),
DEFAULT_ALPHABET.length() - 1)));
+ }
+
@Test
void testEmptyBase58() {
byte[] empty = {};