Package org.eclipse.ecf.protocol.bittorrent

Source Code of org.eclipse.ecf.protocol.bittorrent.TorrentFile

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

}
TOP

Related Classes of org.eclipse.ecf.protocol.bittorrent.TorrentFile

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.