1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.codec.binary;
19
20 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertFalse;
23 import static org.junit.jupiter.api.Assertions.assertNull;
24 import static org.junit.jupiter.api.Assertions.assertThrows;
25 import static org.junit.jupiter.api.Assertions.assertTrue;
26
27 import java.io.IOException;
28 import java.nio.ByteBuffer;
29 import java.nio.charset.Charset;
30 import java.nio.charset.StandardCharsets;
31 import java.util.Arrays;
32 import java.util.Random;
33 import java.util.concurrent.atomic.AtomicInteger;
34
35 import org.apache.commons.codec.DecoderException;
36 import org.apache.commons.codec.EncoderException;
37 import org.apache.commons.lang3.ArrayFill;
38 import org.junit.jupiter.api.Test;
39 import org.junit.jupiter.params.ParameterizedTest;
40 import org.junit.jupiter.params.provider.CsvSource;
41 import org.junit.jupiter.params.provider.ValueSource;
42
43
44
45
46 public class Base58Test {
47
48 private static final int BOUND = 10_000;
49
50 private static final String DEFAULT_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
51
52 private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
53
54 private static void assertArrayEqualsAt(final byte[] data, final byte[] dec, final int i) {
55 final AtomicInteger counter = new AtomicInteger(i);
56 assertArrayEquals(data, dec, () -> String.format("Failed for length %,d: %s", counter.get(), Arrays.toString(data)));
57 }
58
59 private static byte[] fromHex(final String hex) {
60 try {
61 return Hex.decodeHex(hex);
62 } catch (final DecoderException e) {
63 throw new AssertionError("Invalid test-vector hex: " + hex, e);
64 }
65 }
66
67 private static byte[] newEncodeTable() {
68 return DEFAULT_ALPHABET.getBytes(StandardCharsets.US_ASCII);
69 }
70
71 private static byte[] newSwappedEncodeTable() {
72 final byte[] encodeTable = newEncodeTable();
73 final byte tmp = encodeTable[0];
74 encodeTable[0] = encodeTable[1];
75 encodeTable[1] = tmp;
76 return encodeTable;
77 }
78
79 private final Random random = new Random();
80
81 @Test
82 void testBase58() {
83 final String content = "Hello World";
84 final byte[] encodedBytes = new Base58().encode(StringUtils.getBytesUtf8(content));
85 final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
86 assertEquals("JxF12TrwUP45BMd", encodedContent, "encoding hello world");
87 final byte[] decodedBytes = new Base58().decode(encodedBytes);
88 final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
89 assertEquals(content, decodedContent, "decoding hello world");
90 }
91
92 @Test
93 void testBuilderCustomEncodeTableAffectsEncodeAndDecode() {
94 final Base58 base58 = Base58.builder().setEncodeTable(newSwappedEncodeTable()).get();
95 assertEquals("1", new String(base58.encode(new byte[] { 1 }), StandardCharsets.US_ASCII));
96 assertArrayEquals(new byte[] { 1 }, base58.decode("1".getBytes(StandardCharsets.US_ASCII)));
97 }
98
99 @Test
100 void testBuilderCustomEncodeTableAffectsIsInAlphabet() {
101 final byte[] encodeTable = newEncodeTable();
102 encodeTable[0] = '0';
103 final Base58 base58 = Base58.builder().setEncodeTable(encodeTable).get();
104 assertTrue(base58.isInAlphabet((byte) '0'));
105 assertFalse(base58.isInAlphabet((byte) '1'));
106 assertEquals("0", new String(base58.encode(new byte[] { 0 }), StandardCharsets.US_ASCII));
107 assertArrayEquals(new byte[] { 0 }, base58.decode("0".getBytes(StandardCharsets.US_ASCII)));
108 }
109
110 @Test
111 void testBuilderCustomEncodeTableAffectsLeadingZeros() {
112 final Base58 base58 = Base58.builder().setEncodeTable(newSwappedEncodeTable()).get();
113 final byte[] data = { 0, 0, 1 };
114 final byte[] encoded = base58.encode(data);
115 assertEquals("221", new String(encoded, StandardCharsets.US_ASCII));
116 assertArrayEquals(data, base58.decode(encoded));
117 }
118
119 @Test
120 void testBuilderCustomEncodeTableRejectsDuplicateEntries() {
121 final byte[] encodeTable = newEncodeTable();
122 encodeTable[1] = encodeTable[0];
123 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setEncodeTable(encodeTable));
124 }
125
126 @Test
127 void testBuilderCustomEncodeTableRejectsInvalidLength() {
128 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setEncodeTable(Arrays.copyOf(newEncodeTable(), DEFAULT_ALPHABET.length() - 1)));
129 }
130
131 @ParameterizedTest
132 @ValueSource(ints = { 20_000, 40_000, 80_000, 160_000, 320_000 })
133 void testDecodeLargeInput(final int n) {
134
135 Base58.builder().setMaxDecodeLength(n).get().decode(ArrayFill.fill(new byte[n], (byte) 'z'));
136 }
137
138
139
140
141
142 @ParameterizedTest
143 @ValueSource(strings = { "0", "O", "I", "l", "+", "/", " ", "=", "~" })
144 void testDecodeRejectsNonAlphabetCharacters(final String badChar) {
145
146 final byte[] input = ("1" + badChar + "1").getBytes(StandardCharsets.US_ASCII);
147 assertThrows(IllegalArgumentException.class, () -> new Base58().decode(input),
148 "expected rejection of character: " + badChar);
149 }
150
151 @Test
152 void testEmptyBase58() {
153 byte[] empty = {};
154 byte[] result = new Base58().encode(empty);
155 assertEquals(0, result.length, "empty Base58 encode");
156 assertNull(new Base58().encode(null), "empty Base58 encode");
157 empty = new byte[0];
158 result = new Base58().decode(empty);
159 assertEquals(0, result.length, "empty Base58 decode");
160 assertNull(new Base58().decode((byte[]) null), "empty Base58 decode");
161 }
162
163 @Test
164 void testEncodeDecode() {
165 for (int i = 1; i < 5; i++) {
166 final byte[] data = new byte[random.nextInt(BOUND) + 1];
167 Arrays.fill(data, (byte) i);
168 final byte[] enc = Base58.builder().setMaxEncodeLength(BOUND).get().encode(data);
169 final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 2).get().decode(enc);
170 assertArrayEqualsAt(data, dec, i);
171 }
172 }
173
174 @Test
175 void testEncodeDecodeRandom() {
176 for (int i = 1; i < 5; i++) {
177 final byte[] data = new byte[random.nextInt(BOUND) + 1];
178 random.nextBytes(data);
179 final byte[] enc = Base58.builder().setMaxEncodeLength(BOUND).get().encode(data);
180 final byte[] dec = Base58.builder().setMaxDecodeLength(BOUND * 2).get().decode(enc);
181 assertArrayEqualsAt(data, dec, i);
182 }
183 }
184
185 @Test
186 void testEncodeDecodeSmall() {
187 for (int i = 0; i < 12; i++) {
188 final byte[] data = new byte[i];
189 Arrays.fill(data, (byte) i);
190 final byte[] enc = new Base58().encode(data);
191 final byte[] dec = new Base58().decode(enc);
192 assertArrayEqualsAt(data, dec, i);
193 }
194 }
195
196 @Test
197 void testEncodeDecodeSmallRandom() {
198 for (int i = 0; i < 12; i++) {
199 final byte[] data = new byte[i];
200 random.nextBytes(data);
201 final byte[] enc = new Base58().encode(data);
202 final byte[] dec = new Base58().decode(enc);
203 assertArrayEqualsAt(data, dec, i);
204 }
205 }
206
207 @Test
208 void testEncodedLength() {
209 final Base58 codec = new Base58();
210 assertEquals(0, codec.getEncodedLength(new byte[0]));
211 assertEquals(1, codec.getEncodedLength(new byte[] { 0 }));
212 assertEquals(3, codec.getEncodedLength(new byte[] { 0, 0, 0 }));
213 assertEquals(1, codec.getEncodedLength(new byte[] { 57 }));
214 assertEquals(2, codec.getEncodedLength(new byte[] { 58 }));
215 assertEquals(3, codec.getEncodedLength(new byte[] { 0, 58 }));
216 final Random random = new Random(58);
217 for (int length = 1; length <= 256; length++) {
218 final byte[] input = new byte[length];
219 random.nextBytes(input);
220 assertEquals(codec.encode(input).length, codec.getEncodedLength(input));
221 }
222 }
223
224 @Test
225 void testEncodedLengthLimit() {
226 final Base58 codec = Base58.builder().setMaxEncodeLength(10).get();
227 assertEquals(10, codec.getEncodedLength(new byte[10]));
228 assertThrows(IllegalArgumentException.class, () -> codec.getEncodedLength(new byte[11]));
229 assertEquals(11, Base58.builder().setMaxEncodeLength(11).get().getEncodedLength(new byte[11]));
230 }
231
232 @Test
233 void testHexEncoding() {
234 final String hexString = "48656c6c6f20576f726c6421";
235 final byte[] encoded = new Base58().encode(StringUtils.getBytesUtf8(hexString));
236 final byte[] decoded = new Base58().decode(StringUtils.newStringUtf8(encoded));
237 assertEquals("5m7UdtXCfQxGvX2K9dLrkNs7AFMS98qn8", StringUtils.newStringUtf8(encoded), "Hex encoding failed");
238 assertEquals(hexString, StringUtils.newStringUtf8(decoded), "Hex decoding failed");
239 }
240
241
242
243
244
245
246
247
248
249 @ParameterizedTest(name = "[{index}] hex={0}")
250 @CsvSource({
251
252 "61, 2g",
253
254 "626262, a3gV",
255
256 "636363, aPEr",
257
258 "73696d706c792061206c6f6e6720737472696e67, 2cFupjhnEsSn59qHXstmK2ffpLv2",
259
260
261 "00010966776006953d5567439e5e39f86a0d273beed61967f6, 16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM",
262 "516b6fcd0f, ABnLTmg",
263 "bf4f89001e670274dd, 3SEo3LWLoPntC",
264 "572e4794, 3EFU7m",
265 "ecac89cad93923c02321, EJDM8drfXA6uyA",
266 "10c8511e, Rt5zm",
267
268 "00000000000000000000, 1111111111",
269
270 "000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5, 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
271 })
272 void testIetfDraftBase58Vectors(final String hex, final String expectedBase58) {
273 final byte[] binary = fromHex(hex.trim());
274 final String base58 = expectedBase58.trim();
275 final byte[] base58Bytes = base58.getBytes(StandardCharsets.US_ASCII);
276 assertArrayEquals(base58Bytes, new Base58().encode(binary), "encode failed for hex=" + hex.trim());
277 assertArrayEquals(binary, new Base58().decode(base58Bytes), "decode failed for base58=" + base58);
278 }
279
280 @Test
281 void testInvalidCharacters() {
282
283 final byte[] invalidChars = "0OIl".getBytes(CHARSET_UTF8);
284 assertThrows(IllegalArgumentException.class, () -> new Base58().decode(invalidChars));
285 }
286
287 @Test
288 void testIsInAlphabet() {
289 final Base58 base58 = new Base58();
290
291 for (char c = '1'; c <= '9'; c++) {
292 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
293 }
294 for (char c = 'A'; c <= 'H'; c++) {
295 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
296 }
297 for (char c = 'J'; c <= 'N'; c++) {
298 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
299 }
300 for (char c = 'P'; c <= 'Z'; c++) {
301 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
302 }
303 for (char c = 'a'; c <= 'k'; c++) {
304 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
305 }
306 for (char c = 'm'; c <= 'z'; c++) {
307 assertTrue(base58.isInAlphabet((byte) c), "char " + c);
308 }
309
310 assertFalse(base58.isInAlphabet((byte) '0'), "char 0");
311 assertFalse(base58.isInAlphabet((byte) 'O'), "char O");
312 assertFalse(base58.isInAlphabet((byte) 'I'), "char I");
313 assertFalse(base58.isInAlphabet((byte) 'l'), "char l");
314
315 assertFalse(base58.isInAlphabet((byte) -1));
316 assertFalse(base58.isInAlphabet((byte) 0));
317 assertFalse(base58.isInAlphabet((byte) 128));
318 assertFalse(base58.isInAlphabet((byte) 255));
319 }
320
321
322
323
324
325 @ParameterizedTest
326 @ValueSource(ints = { 0, 1, 2, 3, 5, 10 })
327 void testLeadingZeroByteCountPreserved(final int zeros) {
328
329 final byte[] data = new byte[zeros + 3];
330 data[zeros] = 0x01;
331 data[zeros + 1] = 0x02;
332 data[zeros + 2] = 0x03;
333 final byte[] encoded = new Base58().encode(data);
334 int leadingOnes = 0;
335 for (final byte b : encoded) {
336 if (b != '1') {
337 break;
338 }
339 leadingOnes++;
340 }
341 assertEquals(zeros, leadingOnes, "leading '1' count must equal leading zero-byte count");
342 assertArrayEquals(data, new Base58().decode(encoded), "round-trip must preserve leading zeros");
343 }
344
345 @Test
346 void testLeadingZeros() {
347
348 final byte[] input = { 0, 0, 1, 2, 3 };
349 final byte[] encoded = new Base58().encode(input);
350 final String encodedStr = new String(encoded);
351
352 assertTrue(encodedStr.startsWith("11"), "Leading zeros should encode as '1' characters");
353
354 final byte[] decoded = new Base58().decode(encoded);
355 assertArrayEquals(input, decoded, "Decoded should match original including leading zeros");
356 }
357
358 @Test
359 void testLineLength() {
360 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setLineLength(76));
361 assertThrows(IllegalArgumentException.class, () -> Base58.builder().setLineSeparator(new byte[0]).setLineLength(1));
362 for (final int length : new int[] { 0, -1 }) {
363 final Base58 codec = Base58.builder().setLineLength(length).get();
364 assertArrayEquals(new byte[] { '2', '1' }, codec.encode(new byte[] { 58 }));
365 assertEquals(2, codec.getEncodedLength(new byte[] { 58 }));
366 }
367 }
368
369 @Test
370 void testObjectDecodeWithInvalidParameter() {
371 assertThrows(DecoderException.class, () -> new Base58().decode(Integer.valueOf(5)));
372 }
373
374 @Test
375 void testObjectDecodeWithValidParameter() throws Exception {
376 final String original = "Hello World!";
377 final Object o = new Base58().encode(original.getBytes(CHARSET_UTF8));
378 final Base58 base58 = new Base58();
379 final Object oDecoded = base58.decode(o);
380 final byte[] baDecoded = (byte[]) oDecoded;
381 final String dest = new String(baDecoded);
382 assertEquals(original, dest, "dest string does not equal original");
383 }
384
385 @Test
386 void testObjectEncodeWithInvalidParameter() {
387 assertThrows(EncoderException.class, () -> new Base58().encode("Yadayadayada"));
388 }
389
390 @Test
391 void testObjectEncodeWithValidParameter() throws Exception {
392 final String original = "Hello World!";
393 final Object origObj = original.getBytes(CHARSET_UTF8);
394 final Object oEncoded = new Base58().encode(origObj);
395 final byte[] bArray = new Base58().decode((byte[]) oEncoded);
396 final String dest = new String(bArray);
397 assertEquals(original, dest, "dest string does not equal original");
398 }
399
400 @Test
401 void testRoundTrip() {
402 final String[] testStrings = { "", "a", "ab", "abc", "abcd", "abcde", "abcdef", "Hello World", "The quick brown fox jumps over the lazy dog",
403 "1234567890", "!@#$%^&*()" };
404 for (final String test : testStrings) {
405 final byte[] input = test.getBytes(CHARSET_UTF8);
406 final byte[] encoded = new Base58().encode(input);
407 final byte[] decoded = new Base58().decode(encoded);
408 assertArrayEquals(input, decoded, "Round trip failed for: " + test);
409 }
410 }
411
412
413
414
415
416 @ParameterizedTest
417 @ValueSource(ints = { 1, 2, 3, 4, 8, 16 })
418 void testRoundTripAllMaxBytes(final int len) {
419 final byte[] data = new byte[len];
420 Arrays.fill(data, (byte) 0xFF);
421 assertArrayEquals(data, new Base58().decode(new Base58().encode(data)),
422 "round-trip failed for " + len + " x 0xFF bytes");
423 }
424
425 @ParameterizedTest
426 @ValueSource(ints = { 0, 1, 2, 3, 4 })
427 void testRoundtripByte0(final int len) throws IOException {
428
429 final byte[] zeros = new byte[len];
430 final byte[] encoded0s = ArrayFill.fill(zeros.clone(), (byte) '1');
431 assertArrayEquals(encoded0s, Base58.builder().get().encode(zeros));
432 final byte[] decoded = Base58.builder().get().decode(encoded0s);
433 assertArrayEquals(zeros, decoded, () -> String.format("zeros=%s, decoded=%s", Arrays.toString(zeros), Arrays.toString(decoded)));
434 }
435
436 @Test
437 void testSingleBytes() {
438
439 for (int i = 1; i <= 255; i++) {
440 final byte[] data = { (byte) i };
441 final byte[] enc = new Base58().encode(data);
442 final byte[] dec = new Base58().decode(enc);
443 assertArrayEquals(data, dec, "Failed for byte value: " + i);
444 }
445 }
446
447
448
449
450
451
452 @Test
453 void testSingleByteZero() {
454 final byte[] data = { 0 };
455 final byte[] encoded = new Base58().encode(data);
456 assertArrayEquals(new byte[] { '1' }, encoded, "single zero byte must encode as '1'");
457 assertArrayEquals(data, new Base58().decode(encoded), "round-trip of single zero byte");
458 }
459
460 @Test
461 void testTestVectors() {
462 final String content = "Hello World!";
463 final String content1 = "The quick brown fox jumps over the lazy dog.";
464 final long content2 = 0x0000287fb4cdL;
465 final byte[] encodedBytes = new Base58().encode(StringUtils.getBytesUtf8(content));
466 final byte[] encodedBytes1 = new Base58().encode(StringUtils.getBytesUtf8(content1));
467 final byte[] content2Bytes = ByteBuffer.allocate(8).putLong(content2).array();
468 final byte[] content2Trimmed = new byte[6];
469 System.arraycopy(content2Bytes, 2, content2Trimmed, 0, 6);
470 final byte[] encodedBytes2 = new Base58().encode(content2Trimmed);
471 final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
472 final String encodedContent1 = StringUtils.newStringUtf8(encodedBytes1);
473 final String encodedContent2 = StringUtils.newStringUtf8(encodedBytes2);
474 assertEquals("2NEpo7TZRRrLZSi2U", encodedContent, "encoding hello world");
475 assertEquals("USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", encodedContent1);
476 assertEquals("11233QC4", encodedContent2, "encoding 0x0000287fb4cd");
477 final byte[] decodedBytes = new Base58().decode(encodedBytes);
478 final byte[] decodedBytes1 = new Base58().decode(encodedBytes1);
479 final byte[] decodedBytes2 = new Base58().decode(encodedBytes2);
480 final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
481 final String decodedContent1 = StringUtils.newStringUtf8(decodedBytes1);
482 assertEquals(content, decodedContent, "decoding hello world");
483 assertEquals(content1, decodedContent1);
484 assertArrayEquals(content2Trimmed, decodedBytes2, "decoding 0x0000287fb4cd");
485 }
486 }