Skip to content

Commit

Permalink
Merge pull request #65 from filip26/feat/100
Browse files Browse the repository at this point in the history
1.0.0
  • Loading branch information
filip26 authored Sep 16, 2024
2 parents 967c607 + dbb156c commit 037146e
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 68 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.apicatalog</groupId>
<artifactId>copper-multibase</artifactId>
<version>0.6.0-SNAPSHOT</version>
<version>1.0.0</version>
<packaging>jar</packaging>

<name>Copper Multibase</name>
Expand Down
67 changes: 43 additions & 24 deletions src/main/java/com/apicatalog/base/Base58.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,45 @@
import java.util.Arrays;

/**
* Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings.
* Base58 is a way to encode Bitcoin addresses (or arbitrary data) as
* alphanumeric strings.
* <p>
* Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet.
* Note that this is not the same 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>
* <li>A string with non-alphanumeric characters is not as easily accepted as an account number.</li>
* <li>E-mail usually won't line-break if there's no punctuation to break at.</li>
* <li>Doubleclicking selects the whole number as one word if it's all alphanumeric.</li>
* <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>
* <li>A string with non-alphanumeric characters is not as easily accepted as an
* account number.</li>
* <li>E-mail usually won't line-break if there's no punctuation to break
* at.</li>
* <li>Doubleclicking selects the whole number as one word if it's all
* alphanumeric.</li>
* </ul>
* <p>
* However, note that the encoding/decoding runs in O(n&sup2;) time, so it is not useful for large data.
* However, note that the encoding/decoding runs in O(n&sup2;) 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.
* 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 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;
}
Expand All @@ -69,17 +82,19 @@ public static String encode(byte[] input) {
while (zeros < input.length && input[zeros] == 0) {
++zeros;
}
// Convert base-256 digits to base-58 digits (plus conversion to ASCII characters)
// Convert base-256 digits to base-58 digits (plus conversion to ASCII
// characters)
input = Arrays.copyOf(input, input.length); // since we modify it in-place
char[] encoded = new char[input.length * 2]; // upper bound
int outputStart = encoded.length;
for (int inputStart = zeros; inputStart < input.length; ) {
for (int inputStart = zeros; inputStart < input.length;) {
encoded[--outputStart] = ALPHABET[divmod(input, inputStart, 256, 58)];
if (input[inputStart] == 0) {
++inputStart; // optimization - skip leading zeros
}
}
// Preserve exactly as many leading encoded zeros in output as there were leading zeros in input.
// 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;
}
Expand All @@ -100,7 +115,8 @@ public static byte[] decode(String input) {
if (input.length() == 0) {
return new byte[0];
}
// Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits).
// 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);
Expand All @@ -118,7 +134,7 @@ public static byte[] decode(String input) {
// 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; ) {
for (int inputStart = zeros; inputStart < input58.length;) {
decoded[--outputStart] = divmod(input58, inputStart, 58, 256);
if (input58[inputStart] == 0) {
++inputStart; // optimization - skip leading zeros
Expand All @@ -137,15 +153,18 @@ public static BigInteger decodeToBigInteger(String input) {
}

/**
* 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.
* 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 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)
* (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) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/apicatalog/multibase/Multibase.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,9 @@ public boolean equals(Object obj) {
Multibase other = (Multibase) obj;
return prefix == other.prefix;
}

@Override
public String toString() {
return "Multibase [prefix=" + prefix + ", length=" + length + "]";
}
}
81 changes: 38 additions & 43 deletions src/test/java/com/apicatalog/multibase/MultibaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,70 @@

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

public class MultibaseTest {

final MultibaseDecoder DECODER = MultibaseDecoder.getInstance();
static final MultibaseDecoder DECODER = MultibaseDecoder.getInstance();

@ParameterizedTest(name = "{index}: {0}")
@MethodSource("test58Data")
void testBase58BTCEncode(String expected, byte[] data) {
String output = Multibase.BASE_58_BTC.encode(data);
assertEquals(expected, output);
@MethodSource("testData")
void testDecoderDecode(String encoded, byte[] expected, Multibase base) {
assertArrayEquals(expected, DECODER.decode(encoded));
}

@ParameterizedTest(name = "{index}: {0}")
@MethodSource(value = { "test58Data", "testData" })
void testDecode(String encoded, byte[] expected) {
byte[] output = DECODER.decode(encoded);
assertArrayEquals(expected, output);
@MethodSource("testData")
void testDecoderGetBase(String encoded, byte[] expected, Multibase base) {
assertEquals(base, DECODER.getBase(encoded).orElse(null));
assertEquals(base, DECODER.getBase(encoded.charAt(0)).orElse(null));
}

@ParameterizedTest(name = "{index}: {0}")
@MethodSource(value = { "test58Data", "testData" })
void testEncode(String expected, byte[] data) {
Multibase base = DECODER.getBase(expected).orElseThrow(IllegalArgumentException::new);
String encoded = base.encode(data);
assertEquals(expected, encoded);
@MethodSource("testData")
void testDecode(String encoded, byte[] expected, Multibase base) {
assertArrayEquals(expected, base.decode(encoded));
}

@ParameterizedTest(name = "{index}: {0}")
@MethodSource("test58Data")
void testIsBase58BTCEncoded(String encoded, byte[] data) {
assertTrue(Multibase.BASE_58_BTC.isEncoded(encoded));
@MethodSource("testData")
void testEncode(String encoded, byte[] data, Multibase base) {
assertEquals(encoded, base.encode(data));
}

@Test
void testIsNotBase58BTCEncoded() {
assertFalse(Multibase.BASE_58_BTC.isEncoded("abc"));
}

static Stream<Arguments> test58Data() {
return Stream.of(
Arguments.of("zUXE7GvtEk8XTXs1GF8HSGbVA9FCX9SEBPe", "Decentralize everything!!".getBytes()),
Arguments.of("zStV1DL6CwTryKyV", "hello world".getBytes()),
Arguments.of("z7paNL19xttacUY", "yes mani !".getBytes()));
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("testData")
void testIsEncoded(String encoded, byte[] data, Multibase base) {
assertTrue(base.isEncoded(encoded));
}

static Stream<Arguments> testData() {
return Stream.of(
Arguments.of("F666F6F6261", "fooba".getBytes()),
Arguments.of("VCPNMUOG", "foob".getBytes()),
Arguments.of("bmzxw6ytboi", "foobar".getBytes()),
Arguments.of("BMZXW6YTBOI", "foobar".getBytes()),
Arguments.of("CMZXW6YTBOI======", "foobar".getBytes()),
Arguments.of("mZg", "f".getBytes()),
Arguments.of("MZg==", "f".getBytes()),
Arguments.of("mZm8", "fo".getBytes()),
Arguments.of("mZm9v", "foo".getBytes()),
Arguments.of("mZm9vYg", "foob".getBytes()),
Arguments.of("mZm9vYmE", "fooba".getBytes()),
Arguments.of("mZm9vYmFy", "foobar".getBytes()),
Arguments.of("UZm9vYmE=", "fooba".getBytes())
);
Arguments.of("F666F6F6261", "fooba".getBytes(), Multibase.BASE_16_UPPER),
Arguments.of("VCPNMUOG", "foob".getBytes(), Multibase.BASE_32_HEX_UPPER),
Arguments.of("bmzxw6ytboi", "foobar".getBytes(), Multibase.BASE_32),
Arguments.of("BMZXW6YTBOI", "foobar".getBytes(), Multibase.BASE_32_UPPER),
Arguments.of("CMZXW6YTBOI======", "foobar".getBytes(), Multibase.BASE_32_PAD_UPPER),
Arguments.of("mZg", "f".getBytes(), Multibase.BASE_64),
Arguments.of("MZg==", "f".getBytes(), Multibase.BASE_64_PAD),
Arguments.of("mZm8", "fo".getBytes(), Multibase.BASE_64),
Arguments.of("mZm9v", "foo".getBytes(), Multibase.BASE_64),
Arguments.of("mZm9vYg", "foob".getBytes(), Multibase.BASE_64),
Arguments.of("mZm9vYmE", "fooba".getBytes(), Multibase.BASE_64),
Arguments.of("mZm9vYmFy", "foobar".getBytes(), Multibase.BASE_64),
Arguments.of("zUXE7GvtEk8XTXs1GF8HSGbVA9FCX9SEBPe", "Decentralize everything!!".getBytes(), Multibase.BASE_58_BTC),
Arguments.of("zStV1DL6CwTryKyV", "hello world".getBytes(), Multibase.BASE_58_BTC),
Arguments.of("z7paNL19xttacUY", "yes mani !".getBytes(), Multibase.BASE_58_BTC),
Arguments.of("MTXVsdGliYXNlIGlzIGF3ZXNvbWUhIFxvLw==", "Multibase is awesome! \\o/".getBytes(), Multibase.BASE_64_PAD),
Arguments.of("zYAjKoNbau5KiqmHPmSxYCvn66dA1vLmwbt", "Multibase is awesome! \\o/".getBytes(), Multibase.BASE_58_BTC),
Arguments.of("BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP", "Multibase is awesome! \\o/".getBytes(), Multibase.BASE_32_UPPER),
Arguments.of("F4D756C74696261736520697320617765736F6D6521205C6F2F", "Multibase is awesome! \\o/".getBytes(), Multibase.BASE_16_UPPER),
Arguments.of("UZm9vYmE=", "fooba".getBytes(), Multibase.BASE_64_URL_PAD));
}
}

0 comments on commit 037146e

Please sign in to comment.