-
Notifications
You must be signed in to change notification settings - Fork 1
Base58 encoder/decode implemented (to replace com.github.multiformats:java-multibase:x.y.z) #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package ch.admin.bj.swiyu.didtoolbox; | ||
|
|
||
| import java.util.Arrays; | ||
|
|
||
| /** | ||
| * Yet another Java implementation of <a href="https://bitcoinwiki.org/wiki/base58#Base58_Encode_and_Decode">base58btc</a> encoder/decoder. | ||
| * <p> | ||
| * Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings. | ||
| * | ||
| * <p>Note that this is different from the base58 as used by Flickr, which you may find referenced around | ||
| * the Internet. | ||
| * | ||
| * <p>Satoshi explains: why base-58 instead of standard base-64 encoding? | ||
| * | ||
| * <ul> | ||
| * <li>Don't want 0OIl characters that look the same in some fonts and could be used to create | ||
| * visually identical looking account numbers. | ||
| * <li>A string with non-alphanumeric characters is not as easily accepted as an account number. | ||
| * <li>E-mail usually won't line-break if there's no punctuation to break at. | ||
| * <li>Doubleclicking selects the whole number as one word if it's all alphanumeric. | ||
| * </ul> | ||
| * | ||
| * <p>However, note that the encoding/decoding runs in O(n²) time, so it is not useful for | ||
| * large data. | ||
| * | ||
| * <p>The basic idea of the encoding is to treat the data bytes as a large number represented using | ||
| * base-256 digits, convert the number to be represented using base-58 digits, preserve the exact | ||
| * number of leading zeros (which are otherwise lost during the mathematical operations on the | ||
| * numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters. | ||
| */ | ||
| public final class Base58 { | ||
|
|
||
| public static final char[] ALPHABET = | ||
| "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); | ||
| private static final char ENCODED_ZERO = ALPHABET[0]; | ||
| private static final int[] INDEXES = new int[128]; | ||
|
|
||
| static { | ||
| Arrays.fill(INDEXES, -1); | ||
| for (int i = 0; i < ALPHABET.length; i++) { | ||
| INDEXES[ALPHABET[i]] = i; | ||
| } | ||
| } | ||
|
|
||
| private Base58() { | ||
| } | ||
|
|
||
| /** | ||
| * Encodes the given bytes as a base58 string (no checksum is appended). | ||
| * | ||
| * @param input the bytes to encode | ||
| * @return the base58-encoded string | ||
| */ | ||
| public static String encode(byte[] input) { | ||
| if (input.length == 0) { | ||
| return ""; | ||
| } | ||
|
|
||
| // Count leading zeros. | ||
| int zeros = 0; | ||
| while (zeros < input.length && input[zeros] == 0) { | ||
| ++zeros; | ||
| } | ||
|
|
||
| // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) | ||
| var in = Arrays.copyOf(input, input.length); // since we modify it in-place | ||
| char[] encoded = new char[in.length * 2]; // upper bound | ||
| int outputStart = encoded.length; | ||
| for (int inputStart = zeros; inputStart < in.length; ) { | ||
| encoded[--outputStart] = ALPHABET[divmod(in, inputStart, 256, 58)]; | ||
|
Check warning on line 70 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
| if (in[inputStart] == 0) { | ||
| ++inputStart; // optimization - skip leading zeros | ||
|
Check warning on line 72 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
Check warningCode scanning / PMD Avoid reassigning the loop control variable 'inputStart' Warning
Avoid reassigning the loop control variable 'inputStart'
|
||
| } | ||
| } | ||
|
|
||
| // Preserve exactly as many leading encoded zeros in output as there were leading zeros in input. | ||
| while (outputStart < encoded.length && encoded[outputStart] == ENCODED_ZERO) { | ||
| ++outputStart; | ||
| } | ||
|
|
||
| while (--zeros >= 0) { | ||
|
Check warning on line 81 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
Check warningCode scanning / PMD Avoid assignment to zeros in operand Warning
Avoid assignment to zeros in operand
|
||
| encoded[--outputStart] = ENCODED_ZERO; | ||
|
Check warning on line 82 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
Check warningCode scanning / PMD Avoid assignment to zeros in operand Warning
Avoid assignment to outputStart in operand
|
||
| } | ||
|
|
||
| // Return encoded string (including encoded leading zeros). | ||
| return new String(encoded, outputStart, encoded.length - outputStart); | ||
| } | ||
|
|
||
| /** | ||
| * Decodes the given base58 string into the original data bytes. | ||
| * | ||
| * @param input the base58-encoded string to decode | ||
| * @return the decoded data bytes | ||
| */ | ||
| @SuppressWarnings({"PMD.CyclomaticComplexity"}) | ||
| public static byte[] decode(String input) { | ||
|
|
||
| if (input.isEmpty()) { | ||
| return new byte[0]; | ||
| } | ||
|
|
||
| // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). | ||
| byte[] input58 = new byte[input.length()]; | ||
| for (int i = 0; i < input.length(); ++i) { | ||
| char c = input.charAt(i); | ||
| int digit = c < 128 ? INDEXES[c] : -1; | ||
| if (digit < 0) { | ||
| throw new IllegalArgumentException( | ||
| String.format("Invalid character in Base58: 0x%04x", (int) c)); | ||
| } | ||
| input58[i] = (byte) digit; | ||
| } | ||
|
|
||
| // Count leading zeros. | ||
| int zeros = 0; | ||
| while (zeros < input58.length && input58[zeros] == 0) { | ||
| ++zeros; | ||
| } | ||
|
|
||
| // Convert base-58 digits to base-256 digits. | ||
| byte[] decoded = new byte[input.length()]; | ||
| int outputStart = decoded.length; | ||
| for (int inputStart = zeros; inputStart < input58.length; ) { | ||
| decoded[--outputStart] = divmod(input58, inputStart, 58, 256); | ||
|
Check warning on line 123 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
Check warningCode scanning / PMD Avoid assignment to zeros in operand Warning
Avoid assignment to outputStart in operand
|
||
| if (input58[inputStart] == 0) { | ||
| ++inputStart; // optimization - skip leading zeros | ||
|
Check warning on line 125 in src/main/java/ch/admin/bj/swiyu/didtoolbox/Base58.java
|
||
Check warningCode scanning / PMD Avoid reassigning the loop control variable 'inputStart' Warning
Avoid reassigning the loop control variable 'inputStart'
|
||
| } | ||
| } | ||
|
|
||
| // Ignore extra leading zeroes that were added during the calculation. | ||
| while (outputStart < decoded.length && decoded[outputStart] == 0) { | ||
| ++outputStart; | ||
| } | ||
|
|
||
| // Return decoded data (including original number of leading zeros). | ||
| return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); | ||
| } | ||
|
|
||
| /** | ||
| * Divides a number, represented as an array of bytes each containing a single digit in the | ||
| * specified base, by the given divisor. The given number is modified in-place to contain the | ||
| * quotient, and the return value is the remainder. | ||
| * | ||
| * @param number the number to divide | ||
| * @param firstDigit the index within the array of the first non-zero digit (this is used for | ||
| * optimization by skipping the leading zeros) | ||
| * @param base the base in which the number's digits are represented (up to 256) | ||
| * @param divisor the number to divide by (up to 256) | ||
| * @return the remainder of the division operation | ||
| */ | ||
| private static byte divmod(byte[] number, int firstDigit, int base, int divisor) { | ||
| // this is just long division which accounts for the base of the input digits | ||
| int remainder = 0; | ||
| for (int i = firstDigit; i < number.length; i++) { | ||
| int digit = (int) number[i] & 0xFF; | ||
| int temp = remainder * base + digit; | ||
| number[i] = (byte) (temp / divisor); | ||
| remainder = temp % divisor; | ||
| } | ||
| return (byte) remainder; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package ch.admin.bj.swiyu.didtoolbox; | ||
|
|
||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.Collection; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertArrayEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
|
||
| class Base58Test { | ||
|
|
||
| private static Collection<Object[]> data() { | ||
| return Arrays.asList( | ||
| new Object[][]{ | ||
| { | ||
| hexToBytes("1220120F6AF601D46E10B2D2E11ED71C55D25F3042C22501E41D1246E7A1E9D3D8EC"), | ||
| "QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB" // w/out multibase ('z') prefix | ||
| }, | ||
| { | ||
| hexToBytes("1220BA8632EF1A07986B171B3C8FAF0F79B3EE01B6C30BBE15A13261AD6CB0D02E3A"), | ||
| "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy" // w/out multibase ('z') prefix | ||
| }, | ||
| {new byte[1], "1"}, // w/out multibase ('z') prefix | ||
| {new byte[2], "11"}, // w/out multibase ('z') prefix | ||
| {new byte[4], "1111"}, // w/out multibase ('z') prefix | ||
| {new byte[8], "11111111"}, // w/out multibase ('z') prefix | ||
| {new byte[16], "1111111111111111"}, // w/out multibase ('z') prefix | ||
| {new byte[32], "11111111111111111111111111111111"}, // w/out multibase ('z') prefix | ||
| { | ||
| hexToBytes("446563656e7472616c697a652065766572797468696e67212121"), | ||
| "36UQrhJq9fNDS7DiAHM9YXqDHMPfr4EMArvt" // w/out multibase ('z') prefix | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| @MethodSource("data") | ||
| @ParameterizedTest(name = "{index}: {0}, {2}") | ||
| void testEncode(byte[] raw, String encoded) { | ||
| String output = Base58.encode(raw); | ||
| assertEquals(encoded, output, String.format("Expected %s, but got %s", bytesToHex(raw), output)); | ||
| } | ||
|
|
||
| @MethodSource("data") | ||
| @ParameterizedTest(name = "{index}: {0}, {2}") | ||
| void testDecode(byte[] raw, String encoded) { | ||
| byte[] output = Base58.decode(encoded); | ||
| assertArrayEquals( | ||
| raw, output, String.format("Expected %s, but got %s", bytesToHex(raw), bytesToHex(output))); | ||
| } | ||
|
|
||
| // Copied from https://stackoverflow.com/a/140861 | ||
| private static byte[] hexToBytes(String s) { | ||
| int len = s.length(); | ||
| byte[] data = new byte[len / 2]; | ||
| for (int i = 0; i < len; i += 2) { | ||
| data[i / 2] = | ||
| (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); | ||
| } | ||
| return data; | ||
| } | ||
|
|
||
| // Copied from https://stackoverflow.com/a/9855338 | ||
| private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); | ||
|
|
||
| private static String bytesToHex(byte[] bytes) { | ||
| char[] hexChars = new char[bytes.length * 2]; | ||
| for (int j = 0; j < bytes.length; j++) { | ||
| int v = bytes[j] & 0xFF; | ||
| hexChars[j * 2] = HEX_ARRAY[v >>> 4]; | ||
| hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; | ||
| } | ||
| return new String(hexChars); | ||
| } | ||
| } |
Check warning
Code scanning / PMD
Avoid assignment to zeros in operand Warning