Skip to content

Commit bb3c354

Browse files
[Codec 326] Add Base58 support (#422)
* [CODEC-326] Adds encoding for Base58; * [CODEC-326] Adds decoding for Base58; * [CODEC-326] Adds test cases for Base58; * [CODEC-326] Adds hex encoding and decoding test case for Base58; * [CODEC-326] Adds Base58InputStream implementation along with tests; * [CODEC-326] Adds Base58OutputStream implementation along with tests; * [CODEC-326] Adds javadoc for Base58 and Base58OutputStream; * [CODEC-326] Fixes linting issues; * [CODEC-326] Adds Test Vectors test case for Base58; * [CODEC-326] Fixes compilation issues; Removes redundant encoding method setting; * Fix Javadoc since tag * Fix Javadoc since tags * Fix Javadoc since tag Removed the since tag from the Builder class documentation. * Fix Javadoc since tags * Fix Javadoc since tag * Fix Javadoc since tag * Fix Javadoc since tag --------- Co-authored-by: Gary Gregory <garydgregory@users.noreply.github.com>
1 parent fe94f93 commit bb3c354

6 files changed

Lines changed: 1371 additions & 0 deletions

File tree

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.codec.binary;
19+
20+
import java.math.BigInteger;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Map;
23+
import java.util.WeakHashMap;
24+
25+
/**
26+
* Provides Base58 encoding and decoding as commonly used in cryptocurrency and blockchain applications.
27+
* <p>
28+
* Base58 is a binary-to-text encoding scheme that uses a 58-character alphabet to encode data. It avoids
29+
* characters that can be confused (0/O, I/l, +/) and is commonly used in Bitcoin and other blockchain systems.
30+
* </p>
31+
* <p>
32+
* This implementation accumulates data internally until EOF is signaled, at which point the entire input is
33+
* converted using BigInteger arithmetic. This is necessary because Base58 encoding/decoding requires access
34+
* to the complete data to properly handle leading zeros.
35+
* </p>
36+
* <p>
37+
* This class is thread-safe for read operations but the Context object used during encoding/decoding should
38+
* not be shared between threads.
39+
* </p>
40+
* <p>
41+
* The Base58 alphabet is: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
42+
* (excludes: 0, I, O, l)
43+
* </p>
44+
*
45+
* @see Base58InputStream
46+
* @see Base58OutputStream
47+
* @since 1.22.0
48+
*/
49+
public class Base58 extends BaseNCodec {
50+
51+
/**
52+
* Base58 alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
53+
* (excludes: 0, I, O, l)
54+
*/
55+
private static final byte[] ENCODE_TABLE = {
56+
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
57+
'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a',
58+
'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's',
59+
't', 'u', 'v', 'w', 'x', 'y', 'z'
60+
};
61+
/**
62+
* This array is a lookup table that translates Unicode characters drawn from the "Base58 Alphabet"
63+
* into their numeric equivalents (0-57). Characters that are not in the Base58 alphabet are marked
64+
* with -1.
65+
*/
66+
private static final byte[] DECODE_TABLE = {
67+
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
68+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
69+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
70+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f
71+
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, // 30-3f '1'-'9' -> 0-8
72+
-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')
73+
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, // 50-5a 'P'-'Z'
74+
-1, -1, -1, -1, -1, // 5b-5f
75+
-1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, // 60-6f 'a'-'k', 'm'-'o' (skip 'l')
76+
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 70-7a 'p'-'z'
77+
};
78+
private final transient Map<Context, byte[]> accumulated = new WeakHashMap<>();
79+
80+
/**
81+
* Constructs a Base58 codec used for encoding and decoding.
82+
*/
83+
public Base58() {
84+
this(new Builder());
85+
}
86+
87+
/**
88+
* Constructs a Base58 codec used for encoding and decoding with custom configuration.
89+
*
90+
* @param builder the builder with custom configuration
91+
*/
92+
public Base58(final Builder builder) {
93+
super(builder);
94+
}
95+
96+
/**
97+
* Decodes the given Base58 encoded data.
98+
* <p>
99+
* This implementation accumulates data internally. When length &lt; 0 (EOF), the accumulated
100+
* data is converted from Base58 to binary.
101+
* </p>
102+
*
103+
* @param array the byte array containing Base58 encoded data
104+
* @param offset the offset in the array to start from
105+
* @param length the number of bytes to decode, or negative to signal EOF
106+
* @param context the context for this decoding operation
107+
*/
108+
@Override
109+
void decode(byte[] array, int offset, int length, Context context) {
110+
if (context.eof) {
111+
return;
112+
}
113+
114+
if (length < 0) {
115+
context.eof = true;
116+
final byte[] accumulate = accumulated.getOrDefault(context, new byte[0]);
117+
if (accumulate.length > 0) {
118+
convertFromBase58(accumulate, context);
119+
}
120+
accumulated.remove(context);
121+
return;
122+
}
123+
124+
final byte[] accumulate = accumulated.getOrDefault(context, new byte[0]);
125+
final byte[] newAccumulated = new byte[accumulate.length + length];
126+
if (accumulate.length > 0) {
127+
System.arraycopy(accumulate, 0, newAccumulated, 0, accumulate.length);
128+
}
129+
System.arraycopy(array, offset, newAccumulated, accumulate.length, length);
130+
accumulated.put(context, newAccumulated);
131+
}
132+
133+
/**
134+
* Encodes the given binary data as Base58.
135+
* <p>
136+
* This implementation accumulates data internally. When length &lt; 0 (EOF), the accumulated
137+
* data is converted to Base58.
138+
* </p>
139+
*
140+
* @param array the byte array containing binary data to encode
141+
* @param offset the offset in the array to start from
142+
* @param length the number of bytes to encode, or negative to signal EOF
143+
* @param context the context for this encoding operation
144+
*/
145+
@Override
146+
void encode(byte[] array, int offset, int length, Context context) {
147+
if (context.eof) {
148+
return;
149+
}
150+
151+
if (length < 0) {
152+
context.eof = true;
153+
final byte[] accumulate = accumulated.getOrDefault(context, new byte[0]);
154+
convertToBase58(accumulate, context);
155+
accumulated.remove(context);
156+
return;
157+
}
158+
159+
final byte[] accumulate = accumulated.getOrDefault(context, new byte[0]);
160+
final byte[] newAccumulated = new byte[accumulate.length + length];
161+
if (accumulate.length > 0) {
162+
System.arraycopy(accumulate, 0, newAccumulated, 0, accumulate.length);
163+
}
164+
System.arraycopy(array, offset, newAccumulated, accumulate.length, length);
165+
accumulated.put(context, newAccumulated);
166+
}
167+
168+
/**
169+
* Converts accumulated binary data to Base58 encoding.
170+
* <p>
171+
* Uses BigInteger arithmetic to convert the binary data to Base58. Leading zeros in the
172+
* binary data are represented as '1' characters in the Base58 encoding.
173+
* </p>
174+
*
175+
* @param accumulate the binary data to encode
176+
* @param context the context for this encoding operation
177+
* @return the buffer containing the encoded data
178+
*/
179+
private byte[] convertToBase58(byte[] accumulate, Context context) {
180+
final StringBuilder base58 = getStringBuilder(accumulate);
181+
final String encoded = base58.reverse().toString();
182+
183+
final byte[] encodedBytes = encoded.getBytes(StandardCharsets.UTF_8);
184+
final byte[] buffer = ensureBufferSize(encodedBytes.length, context);
185+
System.arraycopy(encodedBytes, 0, buffer, context.pos, encodedBytes.length);
186+
context.pos += encodedBytes.length;
187+
return buffer;
188+
}
189+
190+
/**
191+
* Builds the Base58 string representation of the given binary data.
192+
* <p>
193+
* Converts binary data to a BigInteger and divides by 58 repeatedly to get the Base58 digits.
194+
* Handles leading zeros by counting them and appending '1' for each leading zero byte.
195+
* </p>
196+
*
197+
* @param accumulate the binary data to convert
198+
* @return a StringBuilder with the Base58 representation (not yet reversed)
199+
*/
200+
private StringBuilder getStringBuilder(byte[] accumulate) {
201+
BigInteger value = new BigInteger(1, accumulate);
202+
int leadingZeros = 0;
203+
204+
for (byte b : accumulate) {
205+
if (b == 0) {
206+
leadingZeros++;
207+
} else {
208+
break;
209+
}
210+
}
211+
212+
final StringBuilder base58 = new StringBuilder();
213+
while (value.signum() > 0) {
214+
final BigInteger[] divRem = value.divideAndRemainder(BigInteger.valueOf(58));
215+
base58.append((char) ENCODE_TABLE[divRem[1].intValue()]);
216+
value = divRem[0];
217+
}
218+
219+
for (int i = 0; i < leadingZeros; i++) {
220+
base58.append('1');
221+
}
222+
return base58;
223+
}
224+
225+
/**
226+
* Converts Base58 encoded data to binary.
227+
* <p>
228+
* Uses BigInteger arithmetic to convert the Base58 string to binary data. Leading '1' characters
229+
* in the Base58 encoding represent leading zero bytes in the binary data.
230+
* </p>
231+
*
232+
* @param base58Data the Base58 encoded data
233+
* @param context the context for this decoding operation
234+
* @throws IllegalArgumentException if the Base58 data contains invalid characters
235+
*/
236+
private void convertFromBase58(byte[] base58Data, Context context) {
237+
BigInteger value = BigInteger.ZERO;
238+
int leadingOnes = 0;
239+
240+
for (byte b : base58Data) {
241+
if (b == '1') {
242+
leadingOnes++;
243+
} else {
244+
break;
245+
}
246+
}
247+
248+
final BigInteger base = BigInteger.valueOf(58);
249+
BigInteger power = BigInteger.ONE;
250+
251+
for (int i = base58Data.length - 1; i >= leadingOnes; i--) {
252+
final byte b = base58Data[i];
253+
final int digit = b < DECODE_TABLE.length ? DECODE_TABLE[b] : -1;
254+
255+
if (digit < 0) {
256+
throw new IllegalArgumentException("Invalid character in Base58 string: " + (char) b);
257+
}
258+
259+
value = value.add(BigInteger.valueOf(digit).multiply(power));
260+
power = power.multiply(base);
261+
}
262+
263+
byte[] decoded = value.toByteArray();
264+
265+
if (decoded.length > 1 && decoded[0] == 0) {
266+
final byte[] tmp = new byte[decoded.length - 1];
267+
System.arraycopy(decoded, 1, tmp, 0, tmp.length);
268+
decoded = tmp;
269+
}
270+
271+
final byte[] result = new byte[leadingOnes + decoded.length];
272+
System.arraycopy(decoded, 0, result, leadingOnes, decoded.length);
273+
274+
final byte[] buffer = ensureBufferSize(result.length, context);
275+
System.arraycopy(result, 0, buffer, context.pos, result.length);
276+
context.pos += result.length;
277+
}
278+
279+
/**
280+
* Returns whether or not the {@code octet} is in the Base58 alphabet.
281+
*
282+
* @param value The value to test.
283+
* @return {@code true} if the value is defined in the Base58 alphabet {@code false} otherwise.
284+
*/
285+
@Override
286+
protected boolean isInAlphabet(byte value) {
287+
return value >= 0 && value < DECODE_TABLE.length && DECODE_TABLE[value] != -1;
288+
}
289+
290+
/**
291+
* Builds {@link Base58} instances with custom configuration.
292+
*/
293+
public static class Builder extends AbstractBuilder<Base58, Base58.Builder> {
294+
295+
/**
296+
* Constructs a new Base58 builder.
297+
*/
298+
public Builder() {
299+
super(ENCODE_TABLE);
300+
setDecodeTable(DECODE_TABLE);
301+
}
302+
303+
/**
304+
* Builds a new Base58 instance with the configured settings.
305+
306+
* @return a new Base58 codec
307+
*/
308+
@Override
309+
public Base58 get() {
310+
return new Base58(this);
311+
}
312+
313+
/**
314+
* Creates a new Base58 codec instance.
315+
*
316+
* @return a new Base58 codec
317+
*/
318+
@Override
319+
public Base58.Builder setEncodeTable(final byte... encodeTable) {
320+
super.setDecodeTableRaw(DECODE_TABLE);
321+
return super.setEncodeTable(encodeTable);
322+
}
323+
}
324+
325+
}

0 commit comments

Comments
 (0)