package com.limegroup.gnutella.tigertree;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.InvocationTargetException;
import java.util.Iterator;
import java.util.List;
import junit.framework.Test;
import com.bitzi.util.Base32;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.dime.DIMEGenerator;
import com.limegroup.gnutella.dime.DIMEParser;
import com.limegroup.gnutella.dime.DIMERecord;
import com.limegroup.gnutella.downloader.Interval;
import com.limegroup.gnutella.util.BaseTestCase;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.PrivilegedAccessor;
import com.limegroup.gnutella.util.UUID;
/**
* Unit tests for HashTree
*/
public class HashTreeTest extends BaseTestCase {
private static final String filename =
"com/limegroup/gnutella/metadata/mpg4_golem160x90first120.avi";
private static final File file = CommonUtils.getResourceFile(filename);
// urn & tigertree root from bitcollider
private static final String sha1 =
"urn:sha1:UBJSGDTCVZDSBS4K3ZDQJV5VQ3WTBCOK";
private static final String root32 =
"IXVJNDJ7U3NCMZE5ZWBVCXSMWMFY4ZCXG5LUYAY";
private static HashTree hashTree;
private static HashTree treeFromNetwork;
private static DIMERecord xmlRecord;
private static DIMERecord treeRecord;
private static byte[] written;
public HashTreeTest(String name) {
super(name);
}
public static Test suite() {
return buildTestSuite(HashTreeTest.class);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(suite());
}
public void testLargeFile() throws Throwable {
URN urn = URN.createSHA1Urn(file);
try {
HashTree tree = createHashTree(1780149344l,urn);
fail("shouldn't have read whole file");
}catch(IOException expected){}
}
//Due to the long setup time of creating a TigerTree,
//these tests assign global variables as they go by.
//These tests must all be run in the exact order they're
//written so that they work correctly.
public void testBasicTigerTree() throws Throwable {
assertTrue(file.exists());
URN urn = URN.createSHA1Urn(file);
assertEquals(sha1, urn.toString());
hashTree = createHashTree(file, urn);
// most important test:
// if we get the root hash right, the rest will be working, too
assertEquals(root32, hashTree.getRootHash());
assertEquals(4, hashTree.getDepth());
assertTrue(hashTree.isGoodDepth());
assertEquals("/uri-res/N2X?" + sha1, hashTree.getThexURI());
assertEquals("/uri-res/N2X?" + sha1 + ";" + root32,
hashTree.httpStringValue());
List allNodes = hashTree.getAllNodes();
assertEquals(5, allNodes.size());
List one, two, three, four, five;
one = (List)allNodes.get(0);
two = (List)allNodes.get(1);
three = (List)allNodes.get(2);
four = (List)allNodes.get(3);
five = (List)allNodes.get(4);
assertEquals(five, hashTree.getNodes());
// tree looks like:
// u (root)
// / \
// t s
// / \ \
// q r s
// / \ / \ / \
// l m n o p k
// /\ /\ /\ /\ /\ \
// a b c d e f g h i j k
assertEquals(root32, Base32.encode((byte[])one.get(0)));
assertEquals(2, two.size());
assertEquals(3, three.size());
assertEquals(6, four.size());
assertEquals(11, five.size());
assertEquals(1+2+3+6+11, hashTree.getNodeCount());
}
public void testWriteToStream() throws Throwable {
// Now make sure we can write this record out correctly.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
hashTree.write(baos);
written = baos.toByteArray();
assertEquals(written.length, hashTree.getOutputLength());
// Should be two DIME Records.
ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray());
DIMEParser parser = new DIMEParser(in);
xmlRecord = parser.nextRecord();
treeRecord = parser.nextRecord();
assertTrue(!parser.hasNext());
UUID uuid = verifyXML(xmlRecord);
verifyTree(treeRecord, uuid);
}
public void testReadFromStream() throws Throwable {
// Make sure we can read the tree back in.
treeFromNetwork = HashTree.createHashTree(
new ByteArrayInputStream(written), sha1,
root32, file.length()
);
assertEquals(hashTree.getDepth(), treeFromNetwork.getDepth());
assertEquals(hashTree.getRootHash(), treeFromNetwork.getRootHash());
assertEquals(written.length, hashTree.getOutputLength());
}
public void testVerifyChunk() throws Throwable {
File corrupt = new File("corruptFile");
CommonUtils.copy(file, corrupt);
assertTrue(corrupt.exists());
// Now corrupt the 4th chunk.
int chunkSize = hashTree.getNodeSize();
RandomAccessFile raf = new RandomAccessFile(corrupt, "rw");
raf.seek(3*chunkSize+5);
raf.write(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
// chunks 1-3 are good
byte [] chunk = new byte[chunkSize];
for (int i = 0;i < 3*chunkSize ;i+=chunkSize) {
raf.seek(i);
raf.read(chunk);
assertFalse(hashTree.isCorrupt(new Interval(i,i+chunkSize-1),chunk));
}
// the 4th is corrupt
raf.seek(3*chunkSize);
raf.read(chunk);
assertTrue(hashTree.isCorrupt(new Interval(3*chunkSize,4*chunkSize-1),chunk));
// 5th works
raf.seek(4*chunkSize);
raf.read(chunk);
assertFalse(hashTree.isCorrupt(new Interval(4*chunkSize,5*chunkSize-1),chunk));
raf.close();
corrupt.delete();
}
public void testCorruptedXMLRecord() throws Throwable {
// Easiest way to test is to use existing DIMERecords and then
// hack them up.
DIMERecord corruptedXML = null;
// must have valid data.
String data = new String(xmlRecord.getData());
corruptedXML = createCorruptRecord(xmlRecord, data.substring(1));
try {
HashTree tree = createTree(corruptedXML, treeRecord);
fail("expected exception");
} catch(IOException expected) {}
// all these must be correct or the stream is bad.
checkXML("file size=", "02011981", false);
checkXML("file size=", "abcd", false);
checkXML("segmentsize=", "42", false);
checkXML("segmentsize=", "zef", false);
checkXML("digest algorithm=",
"http://open-content.net/spec/digest/sha1", false);
checkXML("outputsize=", "20", false);
checkXML("outputsize=", "pizza", false);
checkXML("type=", "http://open-content.net/spec/thex/depthfirst",
false);
// depth is not checked heavily.
checkXML("depth=", "1982", true);
checkXML("depth=", "large", true);
// test shareaza's wrong system
replaceXML("SYSTEM", "system", true);
// require that the main element is called hashtree
replaceXML("hashtree>", "random>", false);
// allow unknown additional elements
replaceXML("<hashtree>", "<hashtree><element attribute=\"a\"/>", true);
// allow elements to have random children.
replaceXML("/></hashtree>",
">info</serializedtree></hashtree>", true);
}
private void checkXML(String search, String replace, boolean good)
throws Exception {
String data = new String(xmlRecord.getData());
StringBuffer sb = new StringBuffer(data);
int a = data.indexOf("'", data.indexOf(search));
int b = data.indexOf("'", a+1);
sb.replace(a+1, b, replace);
DIMERecord corrupt = createCorruptRecord(xmlRecord, sb.toString());
try {
createTree(corrupt, treeRecord);
if(!good)
fail("expected exception");
} catch(IOException expected) {
if(good)
throw expected;
}
}
private void replaceXML(String search, String replace, boolean good)
throws Exception {
String data = new String(xmlRecord.getData());
StringBuffer sb = new StringBuffer(data);
int a = -1, b = -1;
while(true) {
a = data.indexOf(search, b+1);
if(a == -1) break;
b = search.length() + a;
sb.replace(a, b, replace);
}
DIMERecord corrupt = createCorruptRecord(xmlRecord, sb.toString());
try {
createTree(corrupt, treeRecord);
if(!good)
fail("expected exception");
} catch(IOException expected) {
if(good)
throw expected;
}
}
public void testCorruptedTreeData() throws Throwable {
byte[] data;
// data is too small to fit a root hash.
data = copyData(treeRecord, 3);
checkTree(data, false);
// the root hash is off.
data = copyData(treeRecord, treeRecord.getData().length);
data[0]++;
checkTree(data, false);
// random bytes in the data are off.
data = copyData(treeRecord, treeRecord.getData().length);
data[24 + 24*2 + 24*3 + 5]++;
checkTree(data, false);
// the last generation is off.
data = copyData(treeRecord, treeRecord.getData().length);
data[data.length-5]++;
checkTree(data, false);
// the root hash is correct, but no other data exists.
// HashTreeHandler.HASH_SIZE==24
data = copyData(treeRecord, 24);
checkTree(data, true);
// we have some full correct generations, but not all.
data = copyData(treeRecord, 24 + 24*2);
checkTree(data, true);
// we have correct data that stops in the middle of a generation.
data = copyData(treeRecord, 24 + 24*2 + 24*1);
checkTree(data, false);
// the data isn't even a multiple of the hash size.
data = copyData(treeRecord, 24 + 24*3 + 1);
checkTree(data, false);
// the data is longer than the ideal depth size would hold.
data = copyData(treeRecord, treeRecord.getData().length + 24 * 2);
checkTree(data, true);
}
private void checkTree(byte[] data, boolean good) throws Throwable {
DIMERecord corrupt = null;
corrupt = createCorruptRecord(treeRecord, data);
try {
createTree(xmlRecord, corrupt);
if(!good)
fail("expected exception");
} catch(IOException expected) {
if(good)
throw expected;
}
}
private byte[] copyData(DIMERecord a, int length) {
byte[] ret = new byte[length];
for(int i = 0; i < ret.length && i < a.getData().length; i++)
ret[i] = a.getData()[i];
return ret;
}
private HashTree createTree(DIMERecord a, DIMERecord b) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DIMEGenerator gen = new DIMEGenerator();
gen.add(a); gen.add(b);
gen.write(out);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
return HashTree.createHashTree(in, sha1, root32, file.length());
}
private DIMERecord createCorruptRecord(DIMERecord base, byte[] data) {
return new DIMERecord((byte)base.getTypeId(),
base.getOptions(),
base.getId(),
base.getType(),
data);
}
private DIMERecord createCorruptRecord(DIMERecord base, String data) {
return new DIMERecord((byte)base.getTypeId(),
base.getOptions(),
base.getId(),
base.getType(),
data.getBytes());
}
private UUID verifyXML(DIMERecord xml) throws Throwable {
// simple tests.
assertEquals(DIMERecord.TYPE_MEDIA_TYPE, xml.getTypeId());
assertEquals("text/xml", xml.getTypeString());
assertEquals("", xml.getIdentifier());
assertEquals(new byte[0], xml.getOptions());
String data = new String(xml.getData(), "UTF-8");
String current;
String test = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<!DOCTYPE hashtree SYSTEM "
+ "\"http://open-content.net/spec/thex/thex.dtd\">"
+ "<hashtree>";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = "<file size='" + file.length() + "' ";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = "segmentsize='" + HashTree.BLOCK_SIZE + "'/>";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = "<digest algorithm='http://open-content.net/spec/digest/tiger'";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = " outputsize='24'/>";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = "<serializedtree depth='" + hashTree.getDepth() + "'";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = " type='http://open-content.net/spec/thex/breadthfirst'";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
test = " uri='uuid:";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
// the uri is the next 36 characters, grab it.
current = data.substring(0, 36);
data = data.substring(36);
UUID uuid = new UUID(current);
test = "'/></hashtree>";
current = data.substring(0, test.length());
data = data.substring(test.length());
assertEquals(test, current);
return uuid;
}
private void verifyTree(DIMERecord tree, UUID uuid) throws Throwable {
// simple tests.
assertEquals(DIMERecord.TYPE_ABSOLUTE_URI, tree.getTypeId());
assertEquals("http://open-content.net/spec/thex/breadthfirst",
tree.getTypeString());
assertEquals("uuid:" + uuid.toString(), tree.getIdentifier());
assertEquals(new byte[0], tree.getOptions());
byte[] data = tree.getData();
int offset = 0;
List allNodes = hashTree.getAllNodes();
for(Iterator genIter = allNodes.iterator(); genIter.hasNext(); ) {
for(Iterator i = ((List)genIter.next()).iterator(); i.hasNext();) {
byte[] current = (byte[])i.next();
for(int j = 0; j < current.length; j++)
assertEquals(data[offset++], current[j]);
}
}
assertEquals(data.length, offset);
}
private HashTree createHashTree(File file, URN sha1) throws Throwable {
Object ret = null;
try {
ret = PrivilegedAccessor.invokeMethod(
HashTree.class, "createHashTree",
new Object[] { new Long(file.length()), new FileInputStream(file),
sha1 },
new Class[] { long.class, InputStream.class, URN.class }
);
} catch(InvocationTargetException ite) {
throw ite.getCause();
}
return (HashTree)ret;
}
private HashTree createHashTree(long size, URN sha1) throws Throwable {
Object ret = null;
try {
ret = PrivilegedAccessor.invokeMethod(
HashTree.class, "createHashTree",
new Object[] { new Long(size), new ByteArrayInputStream(new byte[0]),
sha1 },
new Class[] { long.class, InputStream.class, URN.class }
);
} catch(InvocationTargetException ite) {
throw ite.getCause();
}
return (HashTree)ret;
}
}