diff --git a/src/main/java/io/ipfs/multibase/Base16.java b/src/main/java/io/ipfs/multibase/Base16.java index f6cc182..4978490 100644 --- a/src/main/java/io/ipfs/multibase/Base16.java +++ b/src/main/java/io/ipfs/multibase/Base16.java @@ -1,19 +1,20 @@ package io.ipfs.multibase; public class Base16 { - public static byte[] decode(String hex) - { + + public static byte[] decode(String hex) { byte[] res = new byte[hex.length()/2]; - for (int i=0; i < res.length; i++) - res[i] = (byte) Integer.parseInt(hex.substring(2*i, 2*i+2), 16); + for (int i=0; i < res.length; i++) { + res[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); + } return res; } - public static String encode(byte[] data) - { + public static String encode(byte[] data) { StringBuilder s = new StringBuilder(); - for (byte b : data) + for (byte b : data) { s.append(String.format("%02x", b & 0xFF)); + } return s.toString(); } } diff --git a/src/main/java/io/ipfs/multibase/Base32.java b/src/main/java/io/ipfs/multibase/Base32.java new file mode 100644 index 0000000..081f9bc --- /dev/null +++ b/src/main/java/io/ipfs/multibase/Base32.java @@ -0,0 +1,20 @@ +package io.ipfs.multibase; + +import java.math.BigInteger; + +/** + * Based on RFC 4648 + * No padding + */ +public class Base32 { + private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; + private static final BigInteger BASE = BigInteger.valueOf(32); + + public static String encode(final byte[] input) { + return BaseN.encode(ALPHABET, BASE, input); + } + + public static byte[] decode(final String input) { + return BaseN.decode(ALPHABET, BASE, input); + } +} diff --git a/src/main/java/io/ipfs/multibase/Base58.java b/src/main/java/io/ipfs/multibase/Base58.java index 661d774..ba4102d 100644 --- a/src/main/java/io/ipfs/multibase/Base58.java +++ b/src/main/java/io/ipfs/multibase/Base58.java @@ -36,54 +36,11 @@ public class Base58 { private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; private static final BigInteger BASE = BigInteger.valueOf(58); - public static String encode(byte[] input) { - // TODO: This could be a lot more efficient. - BigInteger bi = new BigInteger(1, input); - StringBuffer s = new StringBuffer(); - while (bi.compareTo(BASE) >= 0) { - BigInteger mod = bi.mod(BASE); - s.insert(0, ALPHABET.charAt(mod.intValue())); - bi = bi.subtract(mod).divide(BASE); - } - s.insert(0, ALPHABET.charAt(bi.intValue())); - // Convert leading zeros too. - for (byte anInput : input) { - if (anInput == 0) - s.insert(0, ALPHABET.charAt(0)); - else - break; - } - return s.toString(); + public static String encode(final byte[] input) { + return BaseN.encode(ALPHABET, BASE, input); } - public static byte[] decode(String input) { - byte[] bytes = decodeToBigInteger(input).toByteArray(); - // We may have got one more byte than we wanted, if the high bit of the next-to-last byte was not zero. This - // is because BigIntegers are represented with twos-compliment notation, thus if the high bit of the last - // byte happens to be 1 another 8 zero bits will be added to ensure the number parses as positive. Detect - // that case here and chop it off. - boolean stripSignByte = bytes.length > 1 && bytes[0] == 0 && bytes[1] < 0; - // Count the leading zeros, if any. - int leadingZeros = 0; - for (int i = 0; input.charAt(i) == ALPHABET.charAt(0); i++) { - leadingZeros++; - } - // Now cut/pad correctly. Java 6 has a convenience for this, but Android can't use it. - byte[] tmp = new byte[bytes.length - (stripSignByte ? 1 : 0) + leadingZeros]; - System.arraycopy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.length - leadingZeros); - return tmp; - } - - public static BigInteger decodeToBigInteger(String input) { - BigInteger bi = BigInteger.valueOf(0); - // Work backwards through the string. - for (int i = input.length() - 1; i >= 0; i--) { - int alphaIndex = ALPHABET.indexOf(input.charAt(i)); - if (alphaIndex == -1) { - throw new IllegalStateException("Illegal character " + input.charAt(i) + " at " + i); - } - bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(BASE.pow(input.length() - 1 - i))); - } - return bi; + public static byte[] decode(final String input) { + return BaseN.decode(ALPHABET, BASE, input); } } diff --git a/src/main/java/io/ipfs/multibase/BaseN.java b/src/main/java/io/ipfs/multibase/BaseN.java new file mode 100644 index 0000000..38d8493 --- /dev/null +++ b/src/main/java/io/ipfs/multibase/BaseN.java @@ -0,0 +1,57 @@ +package io.ipfs.multibase; + +import java.math.BigInteger; + +public class BaseN { + + static String encode(final String alphabet, final BigInteger base, final byte[] input) { + // TODO: This could be a lot more efficient. + BigInteger bi = new BigInteger(1, input); + StringBuffer s = new StringBuffer(); + while (bi.compareTo(base) >= 0) { + BigInteger mod = bi.mod(base); + s.insert(0, alphabet.charAt(mod.intValue())); + bi = bi.subtract(mod).divide(base); + } + s.insert(0, alphabet.charAt(bi.intValue())); + // Convert leading zeros too. + for (byte anInput : input) { + if (anInput == 0) + s.insert(0, alphabet.charAt(0)); + else + break; + } + return s.toString(); + } + + static byte[] decode(final String alphabet, final BigInteger base, final String input) { + byte[] bytes = decodeToBigInteger(alphabet, base, input).toByteArray(); + // We may have got one more byte than we wanted, if the high bit of the next-to-last byte was not zero. This + // is because BigIntegers are represented with twos-compliment notation, thus if the high bit of the last + // byte happens to be 1 another 8 zero bits will be added to ensure the number parses as positive. Detect + // that case here and chop it off. + boolean stripSignByte = bytes.length > 1 && bytes[0] == 0 && bytes[1] < 0; + // Count the leading zeros, if any. + int leadingZeros = 0; + for (int i = 0; input.charAt(i) == alphabet.charAt(0); i++) { + leadingZeros++; + } + // Now cut/pad correctly. Java 6 has a convenience for this, but Android can't use it. + byte[] tmp = new byte[bytes.length - (stripSignByte ? 1 : 0) + leadingZeros]; + System.arraycopy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.length - leadingZeros); + return tmp; + } + + private static BigInteger decodeToBigInteger(final String alphabet, final BigInteger base, final String input) { + BigInteger bi = BigInteger.valueOf(0); + // Work backwards through the string. + for (int i = input.length() - 1; i >= 0; i--) { + int alphaIndex = alphabet.indexOf(input.charAt(i)); + if (alphaIndex == -1) { + throw new IllegalStateException("Illegal character " + input.charAt(i) + " at " + i); + } + bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(base.pow(input.length() - 1 - i))); + } + return bi; + } +} diff --git a/src/main/java/io/ipfs/multibase/Multibase.java b/src/main/java/io/ipfs/multibase/Multibase.java index c86b231..8f4643a 100644 --- a/src/main/java/io/ipfs/multibase/Multibase.java +++ b/src/main/java/io/ipfs/multibase/Multibase.java @@ -1,19 +1,22 @@ package io.ipfs.multibase; -import java.util.*; +import java.util.Map; +import java.util.TreeMap; public class Multibase { public enum Base { - Base1('1'), - Base2('0'), - Base8('7'), - Base10('9'), - Base16('f'), - Base58Flickr('Z'), - Base58BTC('z'); + // encoding(code) + Base1('1'), // unary tends to be 11111 + Base2('0'), // binary has 1 and 0 + Base8('7'), // highest char in octal + Base10('9'), // highest char in decimal + Base16('f'), // highest char in hex + Base32('b'), // rfc4648 no padding + Base58Flickr('Z'), // highest char + Base58BTC('z'); // highest char - public char prefix; + private final char prefix; Base(char prefix) { this.prefix = prefix; @@ -38,6 +41,8 @@ public static String encode(Base b, byte[] data) { return b.prefix + Base58.encode(data); case Base16: return b.prefix + Base16.encode(data); + case Base32: + return b.prefix + Base32.encode(data); default: throw new IllegalStateException("Unsupported base encoding: " + b.name()); } @@ -55,6 +60,8 @@ public static byte[] decode(String data) { return Base58.decode(rest); case Base16: return Base16.decode(rest); + case Base32: + return Base32.decode(rest); default: throw new IllegalStateException("Unsupported base encoding: " + b.name()); } diff --git a/src/test/java/io/ipfs/multibase/MultibaseTest.java b/src/test/java/io/ipfs/multibase/MultibaseTest.java index 362bf8e..14592e0 100755 --- a/src/test/java/io/ipfs/multibase/MultibaseTest.java +++ b/src/test/java/io/ipfs/multibase/MultibaseTest.java @@ -1,8 +1,12 @@ package io.ipfs.multibase; -import org.junit.*; +import org.junit.Test; -import java.util.*; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; public class MultibaseTest { @@ -13,20 +17,47 @@ public void base58Test() { for (String example: examples) { byte[] output = Multibase.decode(example); String encoded = Multibase.encode(Multibase.Base.Base58BTC, output); - if (!encoded.equals(encoded)) - throw new IllegalStateException("Incorrect base58! " + example + " => " + encoded); + assertEquals(example, encoded); } } @Test public void base16Test() { List examples = Arrays.asList("f234abed8debede", - "f87ad873defc2b288a"); + "f87ad873defc2b288", + "f", + "f01", + "f0123456789abcdef"); for (String example: examples) { byte[] output = Multibase.decode(example); String encoded = Multibase.encode(Multibase.Base.Base16, output); - if (!encoded.equals(encoded)) - throw new IllegalStateException("Incorrect base16! " + example + " => " + encoded); + assertEquals(example, encoded); + } + } + + @Test + public void base32Test() { + List examples = Arrays.asList("bnbswy3dpeb3w64tmmq"); + for (String example: examples) { + byte[] output = Multibase.decode(example); + String encoded = Multibase.encode(Multibase.Base.Base32, output); + assertEquals(example, encoded); } } + + @Test + public void invalidBase16Test() { + String example = "f012"; // hex string of odd length + byte[] output = Multibase.decode(example); + String encoded = Multibase.encode(Multibase.Base.Base16, output); + assertNotEquals(example, encoded); + + } + + @Test (expected = NumberFormatException.class) + public void invalidWithExceptionBase16Test() { + String example = "f0g"; // g char is not allowed in hex + Multibase.decode(example); + } + }