View Javadoc
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 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.ValueSource;
41  
42  /**
43   * Tests {@link Base58}.
44   */
45  public class Base58Test {
46  
47      private static final int BOUND = 10_000;
48  
49      private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
50  
51      private static void assertArrayEqualsAt(final byte[] data, final byte[] dec, final int i) {
52          final AtomicInteger counter = new AtomicInteger(i);
53          assertArrayEquals(data, dec, () -> String.format("Failed for length %,d: %s", counter.get(), Arrays.toString(data)));
54      }
55  
56      private final Random random = new Random();
57  
58      @Test
59      void testBase58() {
60          final String content = "Hello World";
61          final byte[] encodedBytes = new Base58().encode(StringUtils.getBytesUtf8(content));
62          final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
63          assertEquals("JxF12TrwUP45BMd", encodedContent, "encoding hello world");
64          final byte[] decodedBytes = new Base58().decode(encodedBytes);
65          final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
66          assertEquals(content, decodedContent, "decoding hello world");
67      }
68  
69      @Test
70      void testEmptyBase58() {
71          byte[] empty = {};
72          byte[] result = new Base58().encode(empty);
73          assertEquals(0, result.length, "empty Base58 encode");
74          assertNull(new Base58().encode(null), "empty Base58 encode");
75          empty = new byte[0];
76          result = new Base58().decode(empty);
77          assertEquals(0, result.length, "empty Base58 decode");
78          assertNull(new Base58().decode((byte[]) null), "empty Base58 decode");
79      }
80  
81      @Test
82      void testEncodeDecode() {
83          for (int i = 1; i < 5; i++) {
84              final byte[] data = new byte[random.nextInt(BOUND) + 1];
85              Arrays.fill(data, (byte) i);
86              final byte[] enc = new Base58().encode(data);
87              final byte[] dec = new Base58().decode(enc);
88              assertArrayEqualsAt(data, dec, i);
89          }
90      }
91  
92      @Test
93      void testEncodeDecodeRandom() {
94          for (int i = 1; i < 5; i++) {
95              final byte[] data = new byte[random.nextInt(BOUND) + 1];
96              random.nextBytes(data);
97              final byte[] enc = new Base58().encode(data);
98              final byte[] dec = new Base58().decode(enc);
99              assertArrayEqualsAt(data, dec, i);
100         }
101     }
102 
103     @Test
104     void testEncodeDecodeSmall() {
105         for (int i = 0; i < 12; i++) {
106             final byte[] data = new byte[i];
107             Arrays.fill(data, (byte) i);
108             final byte[] enc = new Base58().encode(data);
109             final byte[] dec = new Base58().decode(enc);
110             assertArrayEqualsAt(data, dec, i);
111         }
112     }
113 
114     @Test
115     void testEncodeDecodeSmallRandom() {
116         for (int i = 0; i < 12; i++) {
117             final byte[] data = new byte[i];
118             random.nextBytes(data);
119             final byte[] enc = new Base58().encode(data);
120             final byte[] dec = new Base58().decode(enc);
121             assertArrayEqualsAt(data, dec, i);
122         }
123     }
124 
125     @Test
126     void testHexEncoding() {
127         final String hexString = "48656c6c6f20576f726c6421";
128         final byte[] encoded = new Base58().encode(StringUtils.getBytesUtf8(hexString));
129         final byte[] decoded = new Base58().decode(StringUtils.newStringUtf8(encoded));
130         assertEquals("5m7UdtXCfQxGvX2K9dLrkNs7AFMS98qn8", StringUtils.newStringUtf8(encoded), "Hex encoding failed");
131         assertEquals(hexString, StringUtils.newStringUtf8(decoded), "Hex decoding failed");
132     }
133 
134     @Test
135     void testInvalidCharacters() {
136         // Test decoding with invalid characters (those not in Base58 alphabet)
137         final byte[] invalidChars = "0OIl".getBytes(CHARSET_UTF8); // All excluded from Base58
138         assertThrows(IllegalArgumentException.class, () -> new Base58().decode(invalidChars));
139     }
140 
141     @Test
142     void testIsInAlphabet() {
143         final Base58 base58 = new Base58();
144         // Valid characters
145         for (char c = '1'; c <= '9'; c++) {
146             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
147         }
148         for (char c = 'A'; c <= 'H'; c++) {
149             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
150         }
151         for (char c = 'J'; c <= 'N'; c++) {
152             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
153         }
154         for (char c = 'P'; c <= 'Z'; c++) {
155             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
156         }
157         for (char c = 'a'; c <= 'k'; c++) {
158             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
159         }
160         for (char c = 'm'; c <= 'z'; c++) {
161             assertTrue(base58.isInAlphabet((byte) c), "char " + c);
162         }
163         // Invalid characters - excluded from Base58
164         assertFalse(base58.isInAlphabet((byte) '0'), "char 0");
165         assertFalse(base58.isInAlphabet((byte) 'O'), "char O");
166         assertFalse(base58.isInAlphabet((byte) 'I'), "char I");
167         assertFalse(base58.isInAlphabet((byte) 'l'), "char l");
168         // Out of bounds
169         assertFalse(base58.isInAlphabet((byte) -1));
170         assertFalse(base58.isInAlphabet((byte) 0));
171         assertFalse(base58.isInAlphabet((byte) 128));
172         assertFalse(base58.isInAlphabet((byte) 255));
173     }
174 
175     @Test
176     void testLeadingZeros() {
177         // Test that leading zero bytes are encoded as '1' characters
178         final byte[] input = { 0, 0, 1, 2, 3 };
179         final byte[] encoded = new Base58().encode(input);
180         final String encodedStr = new String(encoded);
181         // Should start with "11" (two leading ones for two leading zeros)
182         assertTrue(encodedStr.startsWith("11"), "Leading zeros should encode as '1' characters");
183         // Decode should restore the leading zeros
184         final byte[] decoded = new Base58().decode(encoded);
185         assertArrayEquals(input, decoded, "Decoded should match original including leading zeros");
186     }
187 
188     @Test
189     void testObjectDecodeWithInvalidParameter() {
190         assertThrows(DecoderException.class, () -> new Base58().decode(Integer.valueOf(5)));
191     }
192 
193     @Test
194     void testObjectDecodeWithValidParameter() throws Exception {
195         final String original = "Hello World!";
196         final Object o = new Base58().encode(original.getBytes(CHARSET_UTF8));
197         final Base58 base58 = new Base58();
198         final Object oDecoded = base58.decode(o);
199         final byte[] baDecoded = (byte[]) oDecoded;
200         final String dest = new String(baDecoded);
201         assertEquals(original, dest, "dest string does not equal original");
202     }
203 
204     @Test
205     void testObjectEncodeWithInvalidParameter() {
206         assertThrows(EncoderException.class, () -> new Base58().encode("Yadayadayada"));
207     }
208 
209     @Test
210     void testObjectEncodeWithValidParameter() throws Exception {
211         final String original = "Hello World!";
212         final Object origObj = original.getBytes(CHARSET_UTF8);
213         final Object oEncoded = new Base58().encode(origObj);
214         final byte[] bArray = new Base58().decode((byte[]) oEncoded);
215         final String dest = new String(bArray);
216         assertEquals(original, dest, "dest string does not equal original");
217     }
218 
219     @Test
220     void testRoundTrip() {
221         final String[] testStrings = { "", "a", "ab", "abc", "abcd", "abcde", "abcdef", "Hello World", "The quick brown fox jumps over the lazy dog",
222                 "1234567890", "!@#$%^&*()" };
223         for (final String test : testStrings) {
224             final byte[] input = test.getBytes(CHARSET_UTF8);
225             final byte[] encoded = new Base58().encode(input);
226             final byte[] decoded = new Base58().decode(encoded);
227             assertArrayEquals(input, decoded, "Round trip failed for: " + test);
228         }
229     }
230 
231     @ParameterizedTest
232     @ValueSource(ints = { 0, 1, 2, 3, 4 })
233     void testRoundtripByte0(final int len) throws IOException {
234         // Sanity check, each step from scratch:
235         final byte[] zeros = new byte[len];
236         final byte[] encoded0s = ArrayFill.fill(zeros.clone(), (byte) '1');
237         assertArrayEquals(encoded0s, Base58.builder().get().encode(zeros));
238         final byte[] decoded = Base58.builder().get().decode(encoded0s);
239         assertArrayEquals(zeros, decoded, () -> String.format("zeros=%s, decoded=%s", Arrays.toString(zeros), Arrays.toString(decoded)));
240     }
241 
242     @Test
243     void testSingleBytes() {
244         // Test encoding of single bytes
245         for (int i = 1; i <= 255; i++) {
246             final byte[] data = { (byte) i };
247             final byte[] enc = new Base58().encode(data);
248             final byte[] dec = new Base58().decode(enc);
249             assertArrayEquals(data, dec, "Failed for byte value: " + i);
250         }
251     }
252 
253     @Test
254     void testTestVectors() {
255         final String content = "Hello World!";
256         final String content1 = "The quick brown fox jumps over the lazy dog.";
257         final long content2 = 0x0000287fb4cdL; // Use long to preserve the full 48-bit value
258         final byte[] encodedBytes = new Base58().encode(StringUtils.getBytesUtf8(content));
259         final byte[] encodedBytes1 = new Base58().encode(StringUtils.getBytesUtf8(content1));
260         final byte[] content2Bytes = ByteBuffer.allocate(8).putLong(content2).array();
261         final byte[] content2Trimmed = new byte[6];
262         System.arraycopy(content2Bytes, 2, content2Trimmed, 0, 6);
263         final byte[] encodedBytes2 = new Base58().encode(content2Trimmed);
264         final String encodedContent = StringUtils.newStringUtf8(encodedBytes);
265         final String encodedContent1 = StringUtils.newStringUtf8(encodedBytes1);
266         final String encodedContent2 = StringUtils.newStringUtf8(encodedBytes2);
267         assertEquals("2NEpo7TZRRrLZSi2U", encodedContent, "encoding hello world");
268         assertEquals("USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", encodedContent1);
269         assertEquals("11233QC4", encodedContent2, "encoding 0x0000287fb4cd");
270         final byte[] decodedBytes = new Base58().decode(encodedBytes);
271         final byte[] decodedBytes1 = new Base58().decode(encodedBytes1);
272         final byte[] decodedBytes2 = new Base58().decode(encodedBytes2);
273         final String decodedContent = StringUtils.newStringUtf8(decodedBytes);
274         final String decodedContent1 = StringUtils.newStringUtf8(decodedBytes1);
275         assertEquals(content, decodedContent, "decoding hello world");
276         assertEquals(content1, decodedContent1);
277         assertArrayEquals(content2Trimmed, decodedBytes2, "decoding 0x0000287fb4cd");
278     }
279 }