Skip to content

Commit

Permalink
ObjectHash implementation in Java.
Browse files Browse the repository at this point in the history
Currently only supports JSON Object, Array and String types.
JUnit 4 tests included.

Requires simplebuildtool to build (http://www.scala-sbt.org/)
  • Loading branch information
phad committed Nov 26, 2015
1 parent 9a4baed commit 5246e59
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 1 deletion.
6 changes: 5 additions & 1 deletion 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
Expand All @@ -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
19 changes: 19 additions & 0 deletions 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"
)
205 changes: 205 additions & 0 deletions 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<ObjectHash> {
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<String> 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();
}
}
68 changes: 68 additions & 0 deletions 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());
}
}
}

0 comments on commit 5246e59

Please sign in to comment.