Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
4 changed files
with
297 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} | ||
} |