From 5246e59cc34777ae822ad4a99a24f04336af1aa4 Mon Sep 17 00:00:00 2001 From: Paul Hadfield Date: Thu, 26 Nov 2015 17:19:30 +0000 Subject: [PATCH] ObjectHash implementation in Java. Currently only supports JSON Object, Array and String types. JUnit 4 tests included. Requires simplebuildtool to build (http://www.scala-sbt.org/) --- Makefile | 6 +- build.sbt | 19 ++ .../java/org/links/objecthash/ObjectHash.java | 205 ++++++++++++++++++ .../org/links/objecthash/ObjectHashTest.java | 68 ++++++ 4 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 build.sbt create mode 100644 src/main/java/org/links/objecthash/ObjectHash.java create mode 100644 src/test/java/org/links/objecthash/ObjectHashTest.java diff --git a/Makefile b/Makefile index e60216a..a35a73d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -test: c go python +test: c go java python go: GOPATH=`pwd` go test -v objecthash.go objecthash_test.go @@ -12,5 +12,9 @@ get: c: objecthash_test ./objecthash_test +java: + sbt compile + sbt test + objecthash_test: objecthash_test.c objecthash.c cc -Wall -Werror -I/usr/local/include -o objecthash_test objecthash_test.c objecthash.c crypto-algorithms/sha256.c -L/usr/local/lib -ljson-c diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..a9f9136 --- /dev/null +++ b/build.sbt @@ -0,0 +1,19 @@ +organization := "org.links" + +name := "objecthash" + +version := "1.0-SNAPSHOT" + +autoScalaLibrary := false + +crossPaths := false + +outputStrategy := Some(StdoutOutput) + +mainClass in (Compile, run) := Some ("org.links.objecthash.ObjectHashTest") + +libraryDependencies ++= Seq( + "org.json" % "json" % "20090211", + "junit" % "junit" % "4.10" % "test", + "com.novocode" % "junit-interface" % "0.8" % "test->default" +) diff --git a/src/main/java/org/links/objecthash/ObjectHash.java b/src/main/java/org/links/objecthash/ObjectHash.java new file mode 100644 index 0000000..d383db4 --- /dev/null +++ b/src/main/java/org/links/objecthash/ObjectHash.java @@ -0,0 +1,205 @@ +package org.links.objecthash; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * TODO(phad): docs. + */ +public class ObjectHash implements Comparable { + private static final int SHA256_BLOCK_SIZE = 32; + private static final String SHA256 = "SHA-256"; + + private byte[] hash; + private MessageDigest digester; + + private enum JsonType { + BOOLEAN, + ARRAY, + OBJECT, + INT, + FLOAT, + STRING, + NULL, + UNKNOWN + } + + private ObjectHash() throws NoSuchAlgorithmException { + this.hash = new byte[SHA256_BLOCK_SIZE]; + this.digester = MessageDigest.getInstance(SHA256); + } + + private void hashAny(Object obj) throws NoSuchAlgorithmException, + JSONException { + digester.reset(); + JsonType outerType = getType(obj); + switch (outerType) { + case ARRAY: { + hashList((JSONArray) obj); + break; + } + case OBJECT: { + hashObject((JSONObject) obj); + break; + } + case STRING: { + hashString((String) obj); + break; + } + // TODO(phad): types BOOLEAN, INT, FLOAT, NULL + default: { + throw new IllegalArgumentException("Illegal type in JSON: " + + obj.getClass()); + } + } + + } + + private void hashTaggedBytes(char tag, byte[] bytes) + throws NoSuchAlgorithmException { + digester.reset(); + digester.update((byte) tag); + digester.update(bytes); + hash = digester.digest(); + } + + private void hashString(String str) throws NoSuchAlgorithmException { + hashTaggedBytes('u', str.getBytes()); + } + + + private void hashList(JSONArray list) throws NoSuchAlgorithmException, + JSONException { + digester.reset(); + digester.update((byte) ('l')); + for (int n = 0; n < list.length(); ++n) { + ObjectHash innerObject = new ObjectHash(); + innerObject.hashAny(list.get(n)); + digester.update(innerObject.hash()); + } + hash = digester.digest(); + } + + private void hashObject(JSONObject obj) throws NoSuchAlgorithmException, + JSONException { + ByteBuffer buff = ByteBuffer.allocate(2 * obj.length() * SHA256_BLOCK_SIZE); + Iterator keys = obj.keys(); + while (keys.hasNext()) { + String key = keys.next(); + // TODO(phad): would be nice to chain all these calls builder-stylee. + ObjectHash hKey = new ObjectHash(); + hKey.hashString(key); + ObjectHash hVal = new ObjectHash(); + hVal.hashAny(obj.get(key)); + buff.put(hKey.hash()); + buff.put(hVal.hash()); + } + hashTaggedBytes('d', buff.array()); + } + + private static int parseHex(char digit) { + assert ((digit >= '0' && digit <= '9') || (digit >= 'a' && digit <= 'f')); + if (digit >= '0' && digit <= '9') { + return digit - '0'; + } else { + return 10 + digit - 'a'; + } + } + + public static ObjectHash fromHex(String hex) throws NoSuchAlgorithmException { + ObjectHash h = new ObjectHash(); + hex = hex.toLowerCase(); + if (hex.length() % 2 == 1) { + hex = '0' + hex; + } + // TODO(phad): maybe just use Int.valueOf(s).intValue() + int pos = SHA256_BLOCK_SIZE; + for (int idx = hex.length(); idx > 0; idx -= 2) { + h.hash[--pos] = (byte) (16 * parseHex(hex.charAt(idx - 2)) + + parseHex(hex.charAt(idx - 1))); + } + return h; + } + + private static JsonType getType(Object jsonObj) { + if (jsonObj instanceof JSONArray) { + return JsonType.ARRAY; + } else if (jsonObj instanceof JSONObject) { + return JsonType.OBJECT; + } else if (jsonObj instanceof String) { + return JsonType.STRING; + } else { + return JsonType.UNKNOWN; + } + } + + public static ObjectHash commonJsonHash(String json) + throws JSONException, NoSuchAlgorithmException { + ObjectHash h = new ObjectHash(); + JSONTokener tokener = new JSONTokener(json); + Object outerValue = tokener.nextValue(); + JsonType outerType = getType(outerValue); + switch (outerType) { + case ARRAY: { + h.hashList((JSONArray) outerValue); + break; + } + case OBJECT: { + h.hashObject((JSONObject) outerValue); + break; + } + default: { + throw new IllegalArgumentException("Illegal outer type in JSON: " + + outerType); + } + } + return h; + } + + public static ObjectHash pythonJsonHash(String json) + throws JSONException, NoSuchAlgorithmException { + // TODO(phad): this. + return new ObjectHash(); + } + + @Override + public String toString() { + return this.toHex(); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null) return false; + if (this.getClass() != other.getClass()) return false; + return this.toHex().equals(((ObjectHash) other).toHex()); + } + + @Override + public int compareTo(ObjectHash other) { + return toHex().compareTo(other.toHex()); + } + + public byte[] hash() { + return hash; + } + + public String toHex() { + StringBuffer hexString = new StringBuffer(); + for (int idx = 0; idx < hash.length; ++idx) { + String hex = Integer.toHexString(0xff & hash[idx]); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } +} diff --git a/src/test/java/org/links/objecthash/ObjectHashTest.java b/src/test/java/org/links/objecthash/ObjectHashTest.java new file mode 100644 index 0000000..81592f6 --- /dev/null +++ b/src/test/java/org/links/objecthash/ObjectHashTest.java @@ -0,0 +1,68 @@ +package org.links.objecthash; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ObjectHashTest { + + private void runTest(String json, String expectedHash) throws Exception { + ObjectHash r = ObjectHash.commonJsonHash(json); + ObjectHash e = ObjectHash.fromHex(expectedHash); + assertEquals(e, r); + } + + @Test + public void testEmptyList() throws Exception { + runTest("[]", + "acac86c0e609ca906f632b0e2dacccb2b77d22b0621f20ebece1a4835b93f6f0"); + } + + @Test + public void testListOneString() throws Exception { + runTest("[\"foo\"]", + "268bc27d4974d9d576222e4cdbb8f7c6bd6791894098645a19eeca9c102d0964"); + } + + @Test + public void testListTwoStrings() throws Exception { + runTest("[\"foo\", \"bar\"]", + "32ae896c413cfdc79eec68be9139c86ded8b279238467c216cf2bec4d5f1e4a2"); + } + + @Test + public void testEmptyObject() throws Exception { + runTest("{}", + "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4"); + } + + @Test + public void testListObjectOneStringKeyValue() throws Exception { + runTest("{\"foo\": \"bar\"}\"", + "7ef5237c3027d6c58100afadf37796b3d351025cf28038280147d42fdc53b960"); + } + + @Test + public void testObjectWithListsOfStrings() throws Exception { + runTest("{\"foo\": [\"bar\", \"baz\"], \"qux\": [\"norf\"]}", + "f1a9389f27558538a064f3cc250f8686a0cebb85f1cab7f4d4dcc416ceda3c92"); + + } + + private final static String[][] HEXVALUES = { + {"", "0000000000000000000000000000000000000000000000000000000000000000"}, + {"123", "0000000000000000000000000000000000000000000000000000000000000123"}, + {"abc123", "0000000000000000000000000000000000000000000000000000000000abc123"}, + {"0123456789", "0000000000000000000000000000000000000000000000000000000123456789"}, + {"111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFF0000", + "111122223333444455556666777788889999aaaabbbbccccddddeeeeffff0000"} + }; + + @Test + public void toHexFromHexRoundtrips() throws Exception { + for (String[] hexPair : HEXVALUES) { + assertEquals(hexPair[1], ObjectHash.fromHex(hexPair[0]).toHex()); + } + } +}