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
commit 1e46d8a1a21850218b98e4b58f6af16a86aaee31 Author: Gary Gregory <[email protected]> AuthorDate: Thu Aug 6 12:30:21 2026 -0400 Add Base58 tests --- .../apache/commons/codec/binary/Base58Test.java | 111 +++++++++++++++++++++ 1 file changed, 111 insertions(+) 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 b5ebec5c..4ddc138f 100644 --- a/src/test/java/org/apache/commons/codec/binary/Base58Test.java +++ b/src/test/java/org/apache/commons/codec/binary/Base58Test.java @@ -37,6 +37,7 @@ import org.apache.commons.codec.EncoderException; import org.apache.commons.lang3.ArrayFill; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; /** @@ -55,6 +56,14 @@ public class Base58Test { assertArrayEquals(data, dec, () -> String.format("Failed for length %,d: %s", counter.get(), Arrays.toString(data))); } + private static byte[] fromHex(final String hex) { + try { + return Hex.decodeHex(hex); + } catch (final DecoderException e) { + throw new AssertionError("Invalid test-vector hex: " + hex, e); + } + } + private static byte[] newEncodeTable() { return DEFAULT_ALPHABET.getBytes(StandardCharsets.US_ASCII); } @@ -126,6 +135,19 @@ public class Base58Test { new Base58().decode(ArrayFill.fill(new byte[n], (byte) 'z')); } + /** + * Verifies that characters not in the Base58 alphabet (whitespace, punctuation, excluded + * letters) are rejected during decoding. + */ + @ParameterizedTest + @ValueSource(strings = { "0", "O", "I", "l", "+", "/", " ", "=", "~" }) + void testDecodeRejectsNonAlphabetCharacters(final String badChar) { + // Wrap in a valid prefix/suffix so only the bad char triggers the error. + final byte[] input = ("1" + badChar + "1").getBytes(StandardCharsets.US_ASCII); + assertThrows(IllegalArgumentException.class, () -> new Base58().decode(input), + "expected rejection of character: " + badChar); + } + @Test void testEmptyBase58() { byte[] empty = {}; @@ -191,6 +213,45 @@ public class Base58Test { assertEquals(hexString, StringUtils.newStringUtf8(decoded), "Hex decoding failed"); } + /** + * Tests encode and decode against every test vector in the IETF Base58 encoding draft + * (draft-msporny-base58-03, Appendix A). The hex column is the raw binary input; the second + * column is the expected Base58 output. + * + * @see <a href="https://datatracker.ietf.org/doc/html/draft-msporny-base58-03#appendix-A"> + * draft-msporny-base58-03 Appendix A</a> + */ + @ParameterizedTest(name = "[{index}] hex={0}") + @CsvSource({ + // single byte 'a' (0x61) + "61, 2g", + // "bbb" + "626262, a3gV", + // "ccc" + "636363, aPEr", + // "simply a long string" + "73696d706c792061206c6f6e6720737472696e67, 2cFupjhnEsSn59qHXstmK2ffpLv2", + // leading zero byte + random payload (produces leading '1') + // 25-byte Bitcoin address payload (version + RIPEMD160 hash + checksum) from Bitcoin wiki + "00010966776006953d5567439e5e39f86a0d273beed61967f6, 16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM", + "516b6fcd0f, ABnLTmg", + "bf4f89001e670274dd, 3SEo3LWLoPntC", + "572e4794, 3EFU7m", + "ecac89cad93923c02321, EJDM8drfXA6uyA", + "10c8511e, Rt5zm", + // ten zero bytes -> ten '1' characters + "00000000000000000000, 1111111111", + // 43-byte payload whose Base58 encoding is exactly the full alphabet in order + "000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5, 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + }) + void testIetfDraftBase58Vectors(final String hex, final String expectedBase58) { + final byte[] binary = fromHex(hex.trim()); + final String base58 = expectedBase58.trim(); + final byte[] base58Bytes = base58.getBytes(StandardCharsets.US_ASCII); + assertArrayEquals(base58Bytes, new Base58().encode(binary), "encode failed for hex=" + hex.trim()); + assertArrayEquals(binary, new Base58().decode(base58Bytes), "decode failed for base58=" + base58); + } + @Test void testInvalidCharacters() { // Test decoding with invalid characters (those not in Base58 alphabet) @@ -232,6 +293,30 @@ public class Base58Test { assertFalse(base58.isInAlphabet((byte) 255)); } + /** + * Verifies that the number of leading {@code '1'} characters in the encoded output exactly + * equals the number of leading zero bytes in the input, for a range of counts. + */ + @ParameterizedTest + @ValueSource(ints = { 0, 1, 2, 3, 5, 10 }) + void testLeadingZeroByteCountPreserved(final int zeros) { + // Append a non-zero tail so the total value is non-trivial. + final byte[] data = new byte[zeros + 3]; + data[zeros] = 0x01; + data[zeros + 1] = 0x02; + data[zeros + 2] = 0x03; + final byte[] encoded = new Base58().encode(data); + int leadingOnes = 0; + for (final byte b : encoded) { + if (b != '1') { + break; + } + leadingOnes++; + } + assertEquals(zeros, leadingOnes, "leading '1' count must equal leading zero-byte count"); + assertArrayEquals(data, new Base58().decode(encoded), "round-trip must preserve leading zeros"); + } + @Test void testLeadingZeros() { // Test that leading zero bytes are encoded as '1' characters @@ -288,6 +373,19 @@ public class Base58Test { } } + /** + * Round-trips arrays of all-{@code 0xFF} bytes (maximum unsigned byte value) to confirm + * that bytes with the high bit set are handled correctly. + */ + @ParameterizedTest + @ValueSource(ints = { 1, 2, 3, 4, 8, 16 }) + void testRoundTripAllMaxBytes(final int len) { + final byte[] data = new byte[len]; + Arrays.fill(data, (byte) 0xFF); + assertArrayEquals(data, new Base58().decode(new Base58().encode(data)), + "round-trip failed for " + len + " x 0xFF bytes"); + } + @ParameterizedTest @ValueSource(ints = { 0, 1, 2, 3, 4 }) void testRoundtripByte0(final int len) throws IOException { @@ -310,6 +408,19 @@ public class Base58Test { } } + /** + * Encodes a single zero byte; it must produce the single character {@code '1'}, and the + * round-trip must restore {@code [0x00]}. This byte value is excluded from {@link #testSingleBytes()} + * which starts at {@code 1}. + */ + @Test + void testSingleByteZero() { + final byte[] data = { 0 }; + final byte[] encoded = new Base58().encode(data); + assertArrayEquals(new byte[] { '1' }, encoded, "single zero byte must encode as '1'"); + assertArrayEquals(data, new Base58().decode(encoded), "round-trip of single zero byte"); + } + @Test void testTestVectors() { final String content = "Hello World!";
