diff --git a/.gitignore b/.gitignore index 32858aa..bdb6657 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,8 @@ *.class -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear +build/** +dist/** +.idea/** # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* diff --git a/LICENSE b/LICENSE index e502a90..0d363b0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Multiformats +Copyright (c) 2015 Ian Preston Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 20df057..2243a9d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # java-multihash A Java implementation of Multihash + +## Usage +Multihash m = Multihash.fromBase58("QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy"); + +## Compilation +To compile just run ant. diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..217f0ac --- /dev/null +++ b/build.xml @@ -0,0 +1,74 @@ + + + Java Multihash + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/hamcrest-core-1.3.jar b/lib/hamcrest-core-1.3.jar new file mode 100644 index 0000000..9d5fe16 Binary files /dev/null and b/lib/hamcrest-core-1.3.jar differ diff --git a/lib/junit-4.12.jar b/lib/junit-4.12.jar new file mode 100644 index 0000000..3a7fc26 Binary files /dev/null and b/lib/junit-4.12.jar differ diff --git a/src/main/java/org/ipfs/api/Base58.java b/src/main/java/org/ipfs/api/Base58.java new file mode 100644 index 0000000..1f7ad39 --- /dev/null +++ b/src/main/java/org/ipfs/api/Base58.java @@ -0,0 +1,89 @@ +package org.ipfs.api; + +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.math.BigInteger; + +/** + * A custom form of base58 is used to encode BitCoin addresses. Note that this is not the same base58 as used by + * Flickr, which you may see reference to around the internet.

+ * + * Satoshi says: why base-58 instead of standard base-64 encoding?

+ * + *

+ */ +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 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; + } +} diff --git a/src/main/java/org/ipfs/api/Multihash.java b/src/main/java/org/ipfs/api/Multihash.java new file mode 100644 index 0000000..bab19a4 --- /dev/null +++ b/src/main/java/org/ipfs/api/Multihash.java @@ -0,0 +1,112 @@ +package org.ipfs.api; + +import java.io.*; +import java.util.*; + +public class Multihash { + public enum Type { + sha1(0x11, 20), + sha2_256(0x12, 32), + sha2_512(0x13, 64), + sha3(0x14, 64), + blake2b(0x40, 64), + blake2s(0x41, 32); + + public int index, length; + + Type(int index, int length) { + this.index = index; + this.length = length; + } + + private static Map lookup = new TreeMap<>(); + static { + for (Type t: Type.values()) + lookup.put(t.index, t); + } + + public static Type lookup(int t) { + if (!lookup.containsKey(t)) + throw new IllegalStateException("Unknown Multihash type: "+t); + return lookup.get(t); + } + } + + public final Type type; + private final byte[] hash; + + public Multihash(Type type, byte[] hash) { + if (hash.length > 127) + throw new IllegalStateException("Unsupported hash size: "+hash.length); + if (hash.length != type.length) + throw new IllegalStateException("Incorrect hash length: " + hash.length + " != "+type.length); + this.type = type; + this.hash = hash; + } + + public Multihash(byte[] multihash) { + this(Type.lookup(multihash[0] & 0xff), Arrays.copyOfRange(multihash, 2, multihash.length)); + } + + public byte[] toBytes() { + byte[] res = new byte[hash.length+2]; + res[0] = (byte)type.index; + res[1] = (byte)hash.length; + System.arraycopy(hash, 0, res, 2, hash.length); + return res; + } + + public void serialize(DataOutput dout) throws IOException { + dout.write(toBytes()); + } + + public static Multihash deserialize(DataInput din) throws IOException { + int type = din.readUnsignedByte(); + int len = din.readUnsignedByte(); + Type t = Type.lookup(type); + byte[] hash = new byte[len]; + din.readFully(hash); + return new Multihash(t, hash); + } + + @Override + public String toString() { + return toBase58(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Multihash)) + return false; + return type == ((Multihash) o).type && Arrays.equals(hash, ((Multihash) o).hash); + } + + @Override + public int hashCode() { + return Arrays.hashCode(hash) ^ type.hashCode(); + } + + public String toHex() { + StringBuilder res = new StringBuilder(); + for (byte b: toBytes()) + res.append(String.format("%x", b&0xff)); + return res.toString(); + } + + public String toBase58() { + return Base58.encode(toBytes()); + } + + public static Multihash fromHex(String hex) { + if (hex.length() % 2 != 0) + throw new IllegalStateException("Uneven number of hex digits!"); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + for (int i=0; i < hex.length()-1; i+= 2) + bout.write(Integer.valueOf(hex.substring(i, i+2), 16)); + return new Multihash(bout.toByteArray()); + } + + public static Multihash fromBase58(String base58) { + return new Multihash(Base58.decode(base58)); + } +} diff --git a/src/test/java/org/ipfs/api/MultihashTests.java b/src/test/java/org/ipfs/api/MultihashTests.java new file mode 100755 index 0000000..0e9ea5c --- /dev/null +++ b/src/test/java/org/ipfs/api/MultihashTests.java @@ -0,0 +1,20 @@ +package org.ipfs.api; + +import org.junit.*; + +import java.util.*; + +public class MultihashTests { + + @Test + public void base58Test() { + List examples = Arrays.asList("QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB", + "QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy"); + for (String example: examples) { + byte[] output = Base58.decode(example); + String encoded = Base58.encode(output); + if (!encoded.equals(encoded)) + throw new IllegalStateException("Incorrect base58! " + example + " => " + encoded); + } + } +}