package com.limegroup.gnutella.tigertree; import java.io.IOException; import java.io.RandomAccessFile; import java.io.Serializable; import java.util.Arrays; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.Range; import org.limewire.util.Base32; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.http.HTTPConstants; import com.limegroup.gnutella.security.MerkleTree; import com.limegroup.gnutella.security.Tiger; /** * This class stores HashTrees and is capable of verifying a file it is also * used for storing them in a file. * * Be careful when modifying any non transient variables, as this * class serialized to disk. * * @author Gregorio Roper */ class HashTreeImpl implements Serializable, HashTree { private static final long serialVersionUID = -5752974896215224469L; private static final Log LOG = LogFactory.getLog(HashTreeImpl.class); /** An invalid HashTree. */ public static final HashTreeImpl INVALID = new HashTreeImpl(); /** * The lowest depth list of nodes. */ private final List<byte[]> NODES; /** * The tigertree root hash. */ private final byte[] ROOT_HASH; /** * The size of the file this hash identifies. */ private final long FILE_SIZE; /* * The depth of this tree. */ private final int DEPTH; /** * The URI for this hash tree. */ private final String THEX_URI; /** * The size of each node */ private transient int _nodeSize; /** Constructs an invalid HashTree. */ private HashTreeImpl() { NODES = null; ROOT_HASH = null; FILE_SIZE = -1; DEPTH = -1; THEX_URI = null; } /** * Constructs a new HashTree out of the given nodes, root, sha1 * filesize and chunk size. */ HashTreeImpl(List<List<byte[]>> allNodes, String sha1, long fileSize, int nodeSize) { THEX_URI = HTTPConstants.URI_RES_N2X + sha1; NODES = allNodes.get(allNodes.size()-1); FILE_SIZE = fileSize; ROOT_HASH = allNodes.get(0).get(0); DEPTH = allNodes.size()-1; assert(MerkleTree.log2Ceil(NODES.size()) == DEPTH); assert(NODES.size() * (long)nodeSize >= fileSize); _nodeSize = nodeSize; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isCorrupt(org.limewire.collection.Range, byte[]) */ public boolean isCorrupt(Range in, byte [] data) { return isCorrupt(in, data, data.length); } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isCorrupt(org.limewire.collection.Range, byte[], int) */ public boolean isCorrupt(Range in, byte[] data, int length) { assert(in.getHigh() <= FILE_SIZE); // if the interval is not a fixed chunk, we cannot verify it. // (actually we can but its more complicated) if (in.getLow() % _nodeSize == 0 && in.getHigh() - in.getLow() +1 <= _nodeSize && (in.getHigh() == in.getLow()+_nodeSize-1 || in.getHigh() == FILE_SIZE -1)) { MerkleTree digest = new MerkleTree(new Tiger()); digest.update(data, 0, length); byte [] hash = digest.digest(); byte [] treeHash = NODES.get((int)(in.getLow() / _nodeSize)); boolean ok = Arrays.equals(treeHash, hash); if (LOG.isDebugEnabled()) LOG.debug("interval "+in+" verified "+ok); return !ok; } return true; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isCorrupt(org.limewire.collection.Range, java.io.RandomAccessFile, byte[]) */ public boolean isCorrupt(Range in, RandomAccessFile raf, byte [] tmp) { assert in.getHigh() <= FILE_SIZE : "invalid range "+in+" vs "+FILE_SIZE; // if the interval is not a fixed chunk, we cannot verify it. // (actually we can but its more complicated) if (in.getLow() % _nodeSize == 0 && in.getHigh() - in.getLow() +1 <= _nodeSize && (in.getHigh() == in.getLow()+_nodeSize-1 || in.getHigh() == FILE_SIZE -1)) { try { MerkleTree digest = new MerkleTree(new Tiger()); long read = in.getLow(); while(read <= in.getHigh()) { int size = (int)Math.min(tmp.length, in.getHigh() - read + 1); synchronized(raf) { raf.seek(read); raf.readFully(tmp, 0, size); } digest.update(tmp,0,size); read += size; } byte [] hash = digest.digest(); byte [] treeHash = NODES.get((int)(in.getLow() / _nodeSize)); boolean ok = Arrays.equals(treeHash, hash); if (LOG.isDebugEnabled()) LOG.debug("interval "+in+" verified "+ok); return !ok; } catch (IOException assumeCorrupt) { LOG.debug("iox while verifying ",assumeCorrupt); return true; } } return true; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#httpStringValue() */ public String httpStringValue() { return THEX_URI + ";" + Base32.encode(ROOT_HASH); } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isGoodDepth() */ public boolean isGoodDepth() { return (DEPTH == HashTreeUtils.calculateDepth(FILE_SIZE)); } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isDepthGoodEnough() */ public boolean isDepthGoodEnough() { // for some ranges newDepth actually returns smaller values than oldDepth return DEPTH >= HashTreeUtils.calculateDepth(FILE_SIZE) - 1; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#isBetterTree(com.limegroup.gnutella.tigertree.HashTree) */ public boolean isBetterTree(HashTree other) { if(other == null) return true; else if(other.isGoodDepth()) return false; else if(this.isGoodDepth()) return true; else { int ideal = HashTreeUtils.calculateDepth(FILE_SIZE); int diff1 = Math.abs(this.getDepth() - ideal); int diff2 = Math.abs(other.getDepth() - ideal); if(diff1 < diff2) return true; else return false; } } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getFileSize() */ public long getFileSize() { return FILE_SIZE; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getRootHash() */ public String getRootHash() { return Base32.encode(ROOT_HASH); } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getRootHashBytes() */ public byte[] getRootHashBytes() { return ROOT_HASH; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getTTRootUrn() */ public URN getTreeRootUrn() { try { return URN.createTTRootFromBytes(ROOT_HASH); } catch (IOException notTiger){ } return null; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getThexURI() */ public String getThexURI() { return THEX_URI; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getDepth() */ public int getDepth() { return DEPTH; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getNodes() */ public List<byte[]> getNodes() { return NODES; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getNodeSize() */ public synchronized int getNodeSize() { if (_nodeSize == 0) { // we were deserialized _nodeSize = HashTreeUtils.calculateNodeSize(FILE_SIZE,DEPTH); } return _nodeSize; } /* (non-Javadoc) * @see com.limegroup.gnutella.tigertree.TigerTree#getNodeCount() */ public int getNodeCount() { // This works by calculating how many nodes // will be in the tree based on the number of nodes // at the last depth. The previous depth is always // going to have ceil(current/2) nodes. double last = NODES.size(); int count = (int)last; for(int i = DEPTH-1; i >= 0; i--) { last = Math.ceil(last / 2); count += (int)last; } return count; } }