blob: 4e44e5b9e56c07eb787f7915f5f988c1b62b2158 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2008 Remy Suen, Composent Inc., and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Remy Suen <remy.suen@gmail.com> - initial API and implementation
******************************************************************************/
package org.eclipse.ecf.protocol.bittorrent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import org.eclipse.ecf.protocol.bittorrent.internal.encode.BEncodedDictionary;
import org.eclipse.ecf.protocol.bittorrent.internal.encode.Decode;
/**
* The <code>TorrentFile</code> class is a representation of the information
* stored within a <code>.torrent</code> file. Files can be set with the
* {@link #setTargetFile(File)} method and then have its integrity checked
* against the torrent's hash sum values {@link #validate()} method.
*/
public class TorrentFile {
static MessageDigest shaDigest;
private final String[] filenames;
/**
* An array of Strings that corresponds to the SHA-1 hash of each piece.
*/
private final String[] pieces;
private final long[] lengths;
private final byte[] torrentData;
private final ByteBuffer buffer;
private final BEncodedDictionary dictionary;
private final String tracker;
private final String infoHash;
private final String hexHash;
private File file;
private String name;
private long total;
private final int pieceLength;
private final int numPieces;
static {
try {
shaDigest = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a new <code>Torrent</code> to analyze the provided torrent
* file.
*
* @param file
* the torrent file
* @throws IllegalArgumentException
* If <code>file</code> is <code>null</code> or a directory
* @throws IOException
* If an I/O error occurs whilst analyzing the torrent file
*/
public TorrentFile(File file) throws IllegalArgumentException, IOException {
if (file == null) {
throw new IllegalArgumentException("The file cannot be null"); //$NON-NLS-1$
} else if (file.isDirectory()) {
throw new IllegalArgumentException("The provided file is a directory"); //$NON-NLS-1$
}
name = file.getName();
if (name.endsWith(".torrent")) { //$NON-NLS-1$
name = name.substring(0, name.length() - 8);
}
dictionary = Decode.bDecode(new FileInputStream(file));
torrentData = dictionary.toString().getBytes("ISO-8859-1"); //$NON-NLS-1$
tracker = (String) dictionary.get("announce"); //$NON-NLS-1$
final BEncodedDictionary info = (BEncodedDictionary) dictionary.get("info"); //$NON-NLS-1$
final List list = (List) info.get("files"); //$NON-NLS-1$
if (list != null) {
filenames = new String[list.size()];
lengths = new long[filenames.length];
total = 0;
for (int i = 0; i < filenames.length; i++) {
final BEncodedDictionary aDictionary = (BEncodedDictionary) list.get(i);
lengths[i] = ((Long) aDictionary.get("length")).longValue(); //$NON-NLS-1$
total += lengths[i];
final List aList = (List) aDictionary.get("path"); //$NON-NLS-1$
final StringBuffer buffer = new StringBuffer();
synchronized (buffer) {
for (int j = 0; j < aList.size(); j++) {
buffer.append(aList.get(j)).append(File.separator);
}
}
filenames[i] = buffer.toString();
}
} else {
lengths = new long[] {((Long) info.get("length")).longValue()}; //$NON-NLS-1$
total = lengths[0];
filenames = new String[] {(String) info.get("name")}; //$NON-NLS-1$
}
pieceLength = ((Long) info.get("piece length")).intValue(); //$NON-NLS-1$
buffer = ByteBuffer.allocate(pieceLength);
final String shaPieces = (String) info.get("pieces"); //$NON-NLS-1$
pieces = new String[shaPieces.length() / 20];
for (int i = 0; i < pieces.length; i++) {
pieces[i] = shaPieces.substring(i * 20, i * 20 + 20);
}
numPieces = pieces.length;
infoHash = new String(shaDigest.digest(info.toString().getBytes("ISO-8859-1")), "ISO-8859-1"); //$NON-NLS-1$ //$NON-NLS-2$
final byte[] bytes = infoHash.getBytes("ISO-8859-1"); //$NON-NLS-1$
final StringBuffer hash = new StringBuffer(40);
for (int i = 0; i < bytes.length; i++) {
if (-1 < bytes[i] && bytes[i] < 16) {
hash.append('0');
}
hash.append(Integer.toHexString(0xff & bytes[i]));
}
hexHash = hash.toString();
}
private boolean hashCheckFile() throws FileNotFoundException, IOException {
final int remainder = (int) (file.length() % pieceLength);
int count = 0;
final FileChannel channel = new FileInputStream(file).getChannel();
while (channel.read(buffer) == pieceLength) {
buffer.rewind();
if (!pieces[count].equals(new String(shaDigest.digest(buffer.array()), "ISO-8859-1"))) { //$NON-NLS-1$
return false;
}
count++;
}
buffer.rewind();
shaDigest.update(buffer.array(), 0, remainder);
return pieces[pieces.length - 1].equals(new String(shaDigest.digest(), "ISO-8859-1")); //$NON-NLS-1$
}
private boolean hashCheckFolder() throws FileNotFoundException, IOException {
int read = 0;
int count = 0;
for (int i = 0; i < filenames.length; i++) {
final File download = new File(file.getAbsolutePath(), filenames[i]);
final FileChannel channel = new FileInputStream(download).getChannel();
while ((read += channel.read(buffer)) == pieceLength) {
buffer.rewind();
if (!pieces[count].equals(new String(shaDigest.digest(buffer.array()), "ISO-8859-1"))) { //$NON-NLS-1$
return false;
}
count++;
read = 0;
}
}
buffer.rewind();
shaDigest.update(buffer.array(), 0, read);
return pieces[pieces.length - 1].equals(new String(shaDigest.digest(), "ISO-8859-1")); //$NON-NLS-1$
}
/**
* Checks the integrity of the target file or folder as set by
* {@link #setTargetFile(File)} to determine whether its contents pass all
* of the hash checks.
*
* @return <code>true</code> if and only if every hash check has been
* passed, <code>false</code> otherwise
* @throws FileNotFoundException
* If one of the files associated with the torrent could not be
* found
* @throws IllegalStateException
* If the target file has not been set yet with
* {@link #setTargetFile(File)}
* @throws IOException
* If an I/O error occurs while reading from the files
*/
public boolean validate() throws IllegalStateException, IOException {
if (file == null) {
throw new IllegalStateException("The target file for this torrent has not yet been set"); //$NON-NLS-1$
}
return file.isDirectory() ? hashCheckFolder() : hashCheckFile();
}
/**
* Sets the target file or folder that this torrent should download to or
* look for the corresponding files in.
*
* @param file
* the target file or folder to use, this should be a file if the
* torrent is a single file torrent or a folder if it has
* multiple files
* @throws IllegalArgumentException
* If <code>file</code> is null or if <code>file</code> is a
* directory when this torrent is only using a single file
*/
public void setTargetFile(File file) throws IllegalArgumentException {
if (file == null) {
throw new IllegalArgumentException("The file cannot be null"); //$NON-NLS-1$
} else if (filenames.length == 1 && file.isDirectory()) {
throw new IllegalArgumentException("This torrent is downloading a file, the actual file should be set here and not a directory"); //$NON-NLS-1$
}
this.file = file;
}
/**
* Returns the hash of the <code>info</code> dictionary specified by the
* torrent's metainfo. This is primarily used for torrent identification
* when sending messages to the tracker.
*
* @return the hash of the <code>info</code> dictionary within the
* torrent's metainfo, this will likely contain binary data and will
* not be human-readable as a result
*/
public String getInfoHash() {
return infoHash;
}
/**
* Returns the hexadecimal representation of the hash returned from
* {@link #getInfoHash()}. This string is forty characters long.
*
* @return the hexadecimal value of <code>getInfoHash()</code>
*/
public String getHexHash() {
return hexHash;
}
/**
* Retrieve the specified lengths of the files contained within this
* torrent. Every length contained within the returned array corresponds to
* a file's name specified by {@link #getFilenames()}.
*
* @return an array of lengths for each of the files specified within the
* torrent's metainfo
*/
public long[] getLengths() {
return lengths;
}
/**
* Returns the length of a piece.
*
* @return a piece's length
*/
public int getPieceLength() {
return pieceLength;
}
/**
* Retrieves the URL of the tracker that's handling the requests for this
* torrent.
*
* @return the tracker's URL
*/
public String getTracker() {
return tracker;
}
/**
* Returns a string array that contains the SHA-1 hash of each of the pieces
* defined by the torrent's metainfo.
*
* @return an array of strings with the SHA-1 hash for each piece of data
*/
public String[] getPieces() {
return pieces;
}
/**
* Returns the number of pieces associated with this torrent.
*
* @return the number of pieces
*/
public int getNumPieces() {
return numPieces;
}
/**
* Retrieves the names of all of the files' that is specified by this
* <code>Torrent</code>. All of the files' lengths can be matched up
* against the long value stored within the returned array from
* {@link #getLengths()}.
*
* @return an array of names for all of the files specified by the metadata
*/
public String[] getFilenames() {
return filenames;
}
/**
* Retrieves the name of this torrent file. This is dictated by whatever is
* before the ending <code>.torrent</code> extension. If no such extension
* exists, the entire file's name will be returned.
*
* @return the name of this torrent without the trailing
* <code>.torrent</code> extension, if present
*/
public String getName() {
return name;
}
/**
* Gets the file that has been set as the target file of this torrent per
* {@link #setTargetFile(File)}.
*
* @return the target file for this torrent
*/
public File getTargetFile() {
return file;
}
/**
* Returns whether this torrent is associated with multiple files or not.
*
* @return <code>true</code> if this torrent specifies multiple files,
* <code>false</code> otherwise
*/
public boolean isMultiFile() {
return lengths.length != 1;
}
/**
* Retrieves the total length of all of the files specified within this
* torrent. This is equivalent to the sum of all the lengths returned from
* the array in {@link #getLengths()}.
*
* @return the combined length of all the files specified by this torrent
*/
public long getTotalLength() {
return total;
}
/**
* Writes the contents of the file that was used to initialize this
* <code>TorrentFile</code> onto the provided file.
*
* @param file
* the file to save to
* @throws IOException
* If an I/O error occurs while trying to write to the file
*/
public void save(File file) throws IOException {
final FileOutputStream fos = new FileOutputStream(file);
fos.write(torrentData);
fos.flush();
fos.close();
}
/**
* Returns whether this <code>TorrentFile</code> is equal to the given
* object. The two are the same if their info hash section of the contained
* metainfo is the same. If <code>other</code> is not an instance of a
* <code>TorrentFile</code>, <code>false</code> is returned.
* @param other
*
* @return <code>true</code> if <code>other</code> is a
* <code>TorrentFile</code> and its info hash is the same as this
* one, <code>false</code> otherwise
*/
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (other instanceof TorrentFile) {
return infoHash.equals(((TorrentFile) other).infoHash);
} else {
return false;
}
}
/**
* Returns the hash code of this <code>TorrentFile</code> based on its
* info hash.
*
* @return the result of calling <code>hashCode()</code> on the returned
* string from {@link #getInfoHash()}
*/
public int hashCode() {
return infoHash.hashCode();
}
}