| /******************************************************************************* |
| * 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(); |
| } |
| |
| } |