// Commented for the Learning branch
package com.limegroup.bittorrent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.Constants;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.security.SHA1;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.bittorrent.bencoding.BEncoder;
import com.limegroup.bittorrent.bencoding.Token;
/**
* A BTMetaInfo object represents a .torrent file, and the bencoded information inside.
*
* Give the constructor a clump of Java objects made from parsing bencoded data.
* Then, call methods like getPieceLength() and getFiles() to easily read the information in the bencoded data.
*
* A BTMetaInfo object has references to other objects that are made for each torrent the program is sharing.
* _torrent links up to the ManagedTorrent object that represents the torrent.
* _folder links to a VerifyingFolder that can check hashes and produce the bit field.
*
* This class has some code that goes beyond representing the data inside a .torrent.
* readObject() and writeObject() do serialization to disk.
* addTracker(address) lets you list another tracker alongside those that came from the .torrent file.
* getFiles() returns TorrentFile objects that have paths like "C:\Documents and Settings\Kevin\Incomplete\Torrent Name\Folder\Name.ext".
*/
public class BTMetaInfo implements Serializable {
/** A debugging log we can write lines of text to as the program runs. */
private static final Log LOG = LogFactory.getLog(BTMetaInfo.class);
/** A number that identifies this version of this type of object when it's serialized to a file on the disk. */
static final long serialVersionUID = -2693983731217045071L;
/** Not used. */
private static final ObjectStreamField[] serialPersistentFields = ObjectStreamClass.NO_FIELDS;
/**
* A list of a single all-0 SHA1 hash to use in place of a list of actual file hashes.
*
* FAKE_URN_SET is a HashSet that contains a single URN object, holding a SHA1 hash with 20 0 bytes.
* Written in base 32, 20 bytes of 0s looks like "urn:sha1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".
*
* A FakeFileDesc object uses this list of URNs instead of some that would describe real files.
*/
private static final Set FAKE_URN_SET = new HashSet();
static {
try {
FAKE_URN_SET.add(URN.createSHA1Urn("urn:sha1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));
} catch (IOException ioe) {
ErrorService.error(ioe);
}
}
/**
* The SHA1 hashes of the file pieces.
* _hashes is an array of 20-byte arrays.
*/
private byte[][] _hashes;
/**
* The size in bytes of the pieces this torrent has broken its data into.
* All of the pieces are this size except for the last one, which is probably smaller.
*
* In the .torrent file, this is the bencoded number value of "info"."piece length".
*/
private int _pieceLength;
/**
* The name of the file this .torrent describes, like "File Name.ext".
* For a multifile torrent, this is the name of the folder we should save all the files in, like "Folder Name".
*
* In the .torrent file, this is the bencoded string value of "info"."name".
*/
private String _name;
/**
* An array of TorrentFile objects that contain information about the files listed in this .torrent file.
* If this is a single-file .torrent, there is just 1 TorrentFile object in the array.
*
* A TorrentFile object in this list is like this:
*
* _files[0].LENGTH is the size of the file, in bytes
* _files[0].PATH is the path to where we'll save it like "C:\Documents and Settings\Kevin\Incomplete\Torrent Name\Folder\Name.ext"
* _files[0].begin is the piece number where the file's data begins
* _files[0].end is the piece number where the file's data ends
*/
private TorrentFile[] _files;
/**
* The path to where we're saving this torrent's file, like "C:\Documents and Settings\Kevin\Incomplete\File Name.ext".
* For a multifile torrent, this is the path to the folder, like "C:\Documents and Settings\Kevin\Incomplete\Folder Name".
*
* This is where we'll save the torrent as we're downloading it.
* A Java File object that holds the path.
*/
private File _incompleteFile;
/**
* The path to where we'll move this torrent's file when we've downloaded it completely, like "C:\Documents and Settings\Kevin\Shared\File Name.ext".
* For a multifile torrent, this is the path to the folder, like "C:\Documents and Settings\Kevin\Shared\Folder Name".
*
* This is where we'll move the torrent when it's done.
* A Java File object that holds the path.
*/
private File _completeFile;
/**
* The "info" dictionary of the .torrent file.
* A Java HashMap that contains other Java objects we made by parsing the bencoded data.
*
* A map containing the _infoMap of this torrent, - the bencoded value of
* this map is a unique identifier for the torrent. It is not necessary to
* store it, but at a later date we may want to add support for certain
* extensions stored in this data field
*/
private Map _infoMap;
/**
* The info hash, the hash BitTorrent uses to identify this torrent and its file.
*
* To compute the info hash, take the SHA1 hash of the value of "info" in the bencoded data of the .torrent file.
* The value of "info" is a bencoded dictionary.
*/
private byte[] _infoHash;
/**
* The torrent's info hash as a text URN, like "urn:sha1:JAZSGOLT6UP4I5N5KGJRZPSF6RZCEJKQ".
* The info hash is the SHA1 hash of the "info" section of the bencoded data of the .torrent file.
*/
private URN _infoHashURN;
/**
* This torrent's VerifyingFolder object, which keeps track of which pieces we have and validates their hashes.
*
* Don't serialize _folder to disk.
*/
private VerifyingFolder _folder;
/**
* An array of Java URL objects, each of which is the address of a tracker.
* In the .torrent file, these addresses are listed under "announce".
*
* First, the BTMetaInfo constructor adds the tracker listed under "announce" in the .torrent file.
* Then, TorrentManager.download() calls addTracker(url) to add more that we find out about.
*/
private URL[] _trackers;
/** Always null, not used. */
private Set _locations = null;
/**
* The total size in bytes of the files this .torrent file describes.
* If this is a single file torrent, _totalSize is the file size.
* If this is a multi-file torrent, _totalSize is the size of all the files totaled, and the size of the data block made by putting all the files together.
*/
private long _totalSize;
/** A link to the ManagedTorrent object that represents this torrent. */
private transient ManagedTorrent _torrent = null;
/**
* A FakeFileDesc object with the save path, like "C:\Documents and Settings\Kevin\Shared\File Name.ext".
* This is the kind of object the GUI needs to be able to read the path and list the file.
*/
private transient FileDesc _desc = null;
/**
* Get the size in bytes of the pieces this torrent has broken its data into.
* All of the pieces are this size except for the last one, which is probably smaller.
*
* In the .torrent file, this is the bencoded number value of "info"."piece length".
*
* @return The size of a piece
*/
public int getPieceLength() {
// Get the piece size we parsed
return _pieceLength;
}
/**
* Not used.
*
* @return null
*/
public Set getLocations() {
return _locations;
}
/**
* Get an array of TorrentFile objects that contain information about the files listed in this multifile .torrent file.
* If this is a single-file .torrent, there is just 1 TorrentFile object in the array.
*
* A TorrentFile object in this list is like this:
*
* files[0].LENGTH is the size of the file, in bytes
* files[0].PATH is the path to where we'll save it like "C:\Documents and Settings\Kevin\Incomplete\Torrent Name\Folder\Name.ext"
* files[0].begin is the piece number where the file's data begins
* files[0].end is the piece number where the file's data ends
*
* @return An ArrayList of TorrentFile objects
*/
public TorrentFile[] getFiles() {
// Return the ArrayList that the BTMetaInfo constructor made
return _files;
}
/**
* Get the path to where we're saving this torrent's file, like "C:\Documents and Settings\Kevin\Incomplete\File Name.ext".
* For a multifile torrent, this is the path to the folder, like "C:\Documents and Settings\Kevin\Incomplete\Folder Name".
* This is where we'll save the torrent as we're downloading it.
*
* @return A Java File object that holds the path.
*/
public File getBaseFile() {
// Return the path
return _incompleteFile;
}
/**
* Get the path to where we'll move this torrent's file when we've downloaded it completely, like "C:\Documents and Settings\Kevin\Shared\File Name.ext".
* For a multifile torrent, this is the path to the folder, like "C:\Documents and Settings\Kevin\Shared\Folder Name".
*
* @return A Java File object that holds the path
*/
public File getCompleteFile() {
// If we haven't composed the path, yet, do it and return it
if (_completeFile == null) _completeFile = new File(SharingSettings.getSaveDirectory(), _name); // Get the path to the "Shared" folder
return _completeFile;
}
/**
* Make a FileDesc object with the save path, like "C:\Documents and Settings\Kevin\Shared\File Name.ext".
* This is the kind of object the GUI needs to be able to read the path and list the file.
*
* Returns a new FileDesc object that is actually a FakeFileDesc.
* FakeFileDesc is a nested class in this BTMetaInfo class.
*
* @return A FakeFileDesc object that has the path
*/
public FileDesc getFileDesc() {
// If we haven't made _desc yet, make it now
if (_desc == null) {
// Wrap the path like "C:\Documents and Settings\Kevin\Shared\File Name.ext" into a new FakeFileDesc object
_desc = new FakeFileDesc(
_completeFile == null ? // If we haven't composed the path in the "Shared" folder yet
_incompleteFile : // Use the path with the "Incomplete" folder instead, otherwise
_completeFile); // Use the "Shared" folder path
}
// Return the FileDesc object the GUI needs to read the path
return _desc;
}
/**
* Get the hash of a piece of the file this .torrent describes.
*
* @param pieceNum The piece number, 0 for the first piece
* @return A 20-byte array with the SHA1 hash of that piece of file data
*/
public byte[] getHash(int pieceNum) {
// Look up the 20-byte array in the _hashes array of them
return _hashes[pieceNum];
}
/**
* Get the info hash, the hash BitTorrent uses to identify this torrent and its file.
*
* To compute the info hash, take the SHA1 hash of the value of "info" in the bencoded data of the .torrent file.
* The value of "info" is a bencoded dictionary.
*
* @return The 20-byte SHA1 info hash of this .torrent file
*/
public byte[] getInfoHash() {
// Return the hash the BTMetaInfo constructor computed
return _infoHash;
}
/**
* Get the torrent's info hash as a text URN, like "urn:sha1:JAZSGOLT6UP4I5N5KGJRZPSF6RZCEJKQ".
* The info hash is the SHA1 hash of the "info" section of the bencoded data of the .torrent file.
*
* @return A String like "urn:sha1:JAZSGOLT6UP4I5N5KGJRZPSF6RZCEJKQ"
*/
public URN getURN() {
// Return the String we composed
return _infoHashURN;
}
/**
* Get the VerifyingFolder object this torrent is using to check piece hashes and save files to disk.
*
* @return The VerifyingFolder object
*/
public VerifyingFolder getVerifyingFolder() {
// Return the VerifyingFolder object we saved
return _folder;
}
/**
* Move the torrent file or folder we downloaded from the "Incomplete" folder to the "Shared" folder.
* Adds it to the program's library of files we're sharing.
* Replaces "Incomplete" with "Shared" in the paths in the _files list, and remakes the VerifyingFolder object.
*
* @return false on error
*/
public boolean moveToCompleteFolder() {
// Move the torrent file or folder we downloaded from the "Incomplete" folder to the "Shared" folder, and add it to the program's library of files we're sharing
if (!saveFile(_incompleteFile, SharingSettings.DIRECTORY_FOR_SAVING_FILES.getValue())) return false; // Returns false on error
// Make a Java File object with the destionation path, like "C:\Documents and Settings\Kevin\Shared\File Name.ext"
_completeFile = new File(SharingSettings.DIRECTORY_FOR_SAVING_FILES.getValue(), _name);
// We don't need the FakeFileDesc to show to LimeWire's GUI anymore
_desc = null;
// Replace "Incomplete" with "Shared" in the paths to files in the _files list
updateReferences(_completeFile);
// Remake this BTMetaInfo object's VerifyingFolder object now that we've moved the files
LOG.trace("saved files");
initializeVerifyingFolder(null, true); // true, we're done downloading this torrent
LOG.trace("initialized folder");
// Report success
return true;
}
/**
* Send a Have message with the given piece number to all our connections sharing this torrent.
* If the VerifyingFolder has downloaded the complete torrent, move it into our "Shared" folder.
*
* Have this BTMetaInfo object tell its ManagedTorrent object that we've downloaded and verified a piece of this torrent.
* VerifyingFolder.notifyOfChunkCompletion(pieceNum) calls this.
* Calls ManagedTorrent.notifyOfComplete(pieceNum).
*
* @param pieceNum The piece we just got
*/
public void notifyOfComplete(int pieceNum) {
// If we have a reference to our ManagedTorrent object, tell it
if (_torrent != null) _torrent.notifyOfComplete(pieceNum);
}
/**
* Give this BTMetaInfo object a reference to the ManagedTorrent that represents the same .torrent file.
* We'll inform this ManagedTorrent object about ranges of completed data.
*
* @param torrent The ManagedTorrent reference to save
*/
public void setManagedTorrent(ManagedTorrent torrent) {
// Save the given reference
_torrent = torrent;
}
/**
* Get the total size in bytes of the files this .torrent file describes.
*
* If this is a single file torrent, _totalSize is the file size.
* If this is a multi-file torrent, _totalSize is the size of all the files totaled, and the size of the data block made by putting all the files together.
*
* @return The number of bytes of data of this .torrent file describes
*/
public long getTotalSize() {
// Return the size we read or totaled
return _totalSize;
}
/**
* Total the lengths in an array of TorrentFile objects.
* This is the total size of all of the files that a .torrent file describes.
*
* @param files An array of TorrentFile objects
* @return The total of the length number each one keeps
*/
private static long calculateTotalSize(TorrentFile[] files) {
// Loop for each TorrentFile in the array
long ret = 0; // Start our total at 0
for (int i = 0; i < files.length; i++) {
// Add this TorrentFile's LENGTH to our total
ret += files[i].LENGTH;
}
// Return the total size we summed
return ret;
}
/**
* Calculate the number of pieces this .torrent has broken its file into.
*
* @return The number of pieces
*/
public int getNumBlocks() {
// Calculate the number of pieces, including the last fragment piece
return (int)((_totalSize + _pieceLength - 1) / _pieceLength);
}
/**
* Get the "info" dictionary of the .torrent file.
*
* @return A Java HashMap that contains other Java objects we made by parsing the bencoded data
*/
public Map getInfo() {
// Return the HashMap we parsed
return _infoMap;
}
/**
* Get the name of the file this .torrent describes, like "File Name.ext".
* For a multifile torrent, this is the name of the folder we should save all the files in, like "Folder Name".
*
* In the .torrent file, this is the bencoded string value of "info"."name".
*
* @return The name text as a String
*/
public String getName() {
// Return the name text we parsed
return _name;
}
/**
* Get the Web addresses of the trackers described in this .torrent file.
* These addresses are listed under "announce" in the bencoded data.
*
* @return An array of Java URL objects
*/
public URL[] getTrackers() {
// Return the array we made
return _trackers;
}
/**
* Add the Web address of another tracker to the list of them this BTMetaInfo keeps.
* A BTMetaInfo is made from a .torrent file, and starts out with the one tracker address there.
* TorrentManager.download() calls this to add more to the list.
*
* @param url The Web address of another tracker keeping track of this torrent
* @return false if we already have that tracker
* true if we didn't, and added it
*/
public boolean addTracker(URL url) {
// Make sure we don't already have that tracker
for (int i = 0; i < _trackers.length; i++) if (_trackers[i].equals(url)) return false;
// Add it to the _trackers list, and return true
URL[] newTrackers = new URL[_trackers.length + 1];
System.arraycopy(_trackers, 0, newTrackers, 0, _trackers.length);
newTrackers[_trackers.length] = url;
_trackers = newTrackers;
return true;
}
/**
* Returns a new SHA1 object to signify that BTMetaInfo and BitTorrent use SHA1 to hash the "info" part of the .torrent file into the info hash.
*
* @return A new blank SHA1 object
*/
public MessageDigest getMessageDigest() {
// Make and return a new empty SHA1 object
return new SHA1();
}
/**
* Make a bitfield that shows what pieces of this torrent we have.
* The bitfield will have 1 bit for each piece in the torrent.
* If a bit is 1, it means we have that piece and have checked its hash, 0 means we still need it.
*
* Gets the bitfield from the VerifyingFolder object for this torrent.
* Sends it to the remote computer in a Bit Field message.
*
* @return A byte array with the bit field
*/
public byte[] createBitField() {
// Have our VerifyingFolder object compose the bit field, and return it
return _folder.createBitField();
}
/**
* Turn the data of a .torrent file into a new BTMetaInfo object that represents it.
*
* @param torrent A byte array with the contents of a .torrent file we opened or downloaded
* @return A new BTMetaInfo that represents it
*/
public static BTMetaInfo readFromBytes(byte[] torrent) throws IOException {
// Parse the bencoded data of the .torrent file into a Java object that references others to express its structure
Object metaInfo = Token.parse(torrent); // Returns a Java Map that contains other Java objects
// Use the clump of Java objects to make a BTMetaInfo object
return new BTMetaInfo(metaInfo);
}
/**
* Move a file or folder from the "Incomplete" folder to the "Shared" folder, and add it to the program's library of files we're sharing.
*
* @param incFile The path to a file or folder in the "Incomplete" folder
* @param completeParent The path to the folder in the "Shared" folder where we should move it
* @return True if successful, false on error
*/
private boolean saveFile(File incFile, File completeParent) {
// Call getCanonicalFile() to resolve any navigational codes in the path, like "./"
try {
completeParent = completeParent.getCanonicalFile();
} catch (IOException ioe) {
if (LOG.isDebugEnabled()) LOG.debug(ioe);
return false;
}
// Make sure we can write to the destination folder
FileUtils.setWriteable(completeParent);
// If the given source path points to a folder, have saveDirectory() move it instead of us
if (incFile.isDirectory()) return saveDirectory(incFile, completeParent);
/*
* If control reaches here, we're moving a file
*/
// Get the size of the file on the disk in bytes
long incLength = incFile.length();
// Make completeFile the file's destination path
File completeFile = new File(completeParent, incFile.getName());
// Move the file from incFile to completeFile
completeFile.delete(); // Delete a file that's already there
FileUtils.setWriteable(completeFile); // There shouldn't be a file there to set writable
if (!FileUtils.forceRename(incFile, completeFile)) { // Move the file from incFile to completeFile
LOG.debug("could not rename file " + incFile);
return false;
}
// Make sure that didn't change the file size
if (incLength != completeFile.length()) {
LOG.debug("length of complete file does not match incomplete file " + completeFile + " , " + incLength + ":" + completeFile.length());
return false;
}
// Add the file to LimeWire's Library of files we're sharing on Gnutella
RouterService.getFileManager().removeFileIfShared(completeFile);
RouterService.getFileManager().addFileIfShared(completeFile);
// Report success
return true;
}
/**
* Make a new VerifyingFolder object from this BTMetaInfo one.
* Saves it as _folder.
*
* @param data A Java Map that represents the serialized form of a VerifyingFolder object we read from a file on the disk
* @param complete True if we're done downloading this torrent
*/
private void initializeVerifyingFolder(Map data, boolean complete) {
// Make a new VerifyingFolder object that represents the save folder and can check the hash of file pieces
_folder = new VerifyingFolder(
this, // Give the constructor a link back to this BTMetaInfo object
complete, // True if we're done downloading this torrent
data); // Serialized disk data, use this instead if we have it
}
/**
* Move a folder of files from the "Incomplete" folder to the "Shared" folder, and add it to the program's library of files we're sharing.
*
* @param incFile The path to a folder in the "Incomplete" folder
* @param completeParent The path to the folder in the "Shared" folder where we should move it
* @return True if successful, false on error
*/
private boolean saveDirectory(File incFile, File completeParent) {
// Compose the folder's destination path
File completeDir = new File(completeParent, incFile.getName());
// Make the destination folder, deleting a file already there
if (completeDir.exists()) { // If there is a file at the destination path, delete it
if (!completeDir.isDirectory()) {
if (!(completeDir.delete() && completeDir.mkdirs())) {
LOG.debug("could not create complete dir " + completeDir);
return false;
}
}
} else if (!completeDir.mkdirs()) { // The path is open, but we can't make a folder there
LOG.debug("could not create complete dir " + completeDir);
return false;
}
FileUtils.setWriteable(completeDir); // Set the destination folder writable
// Share the folder before we start putting files into it
RouterService.getFileManager().addFileIfShared(completeDir);
// Loop for each file and folder in the source folder
File[] files = incFile.listFiles();
for (int i = 0; i < files.length; i++) {
// Move it into the destination folder we made
if (!saveFile(files[i], completeDir)) return false; // Return false on error
}
// Delete the source folder we emptied
FileUtils.deleteRecursive(incFile);
// Report success
return true;
}
/**
* Replace "Incomplete" with "Shared" in the paths to files in the _files list.
*
* @param completeBase The path we moved this torrent to when it finished, like "C:\Documents and Settings\Kevin\Shared\File Name.ext"
*/
private void updateReferences(File completeBase) {
// _incompleteFile is like "C:\Documents and Settings\Kevin\Incomplete\File Name.ext", get the length of that string
int offset = _incompleteFile.getAbsolutePath().length();
// Make newPath like "C:\Documents and Settings\Kevin\Shared\File Name.ext"
String newPath = completeBase.getAbsolutePath();
// Loop for each of the files this .torrent file describes
for (int i = 0; i < _files.length; i++) { // If this is a single-file torrent, this loop will just run once
// Change the path of this file from the "Incomplete" folder to the "Shared" folder
_files[i] = new TorrentFile(_files[i].LENGTH, newPath + _files[i].PATH.substring(offset));
}
}
/**
* Make a new BTMetaInfo object to hold the information in a .torrent file.
*
* The program has already opened a .torrent file, and parsed its bencoded dictionary into a Java HashMap object.
* This constructor looks through that clump of objects, making a BTMetaInfo object with the information in the .torrent file.
*
* This constructor takes a Java object.
* Token.parse() turned the bencoded data from a .torrent file, and parsed it into a clump of Java objects.
* Since a .torrent contains a bencoded dictionary, t_metaInfo will be a Java HashMap.
*
* This constructor is private, and only readFromBytes() calls it.
* To make a new BTMetaInfo object, call the static method BTMetaInfo.readFromBytes().
*
* @param t_metaInfo A Java HashMap that contains other Java objects.
* @throws ValueException A part of the .torent file we looked for is missing or wrong.
*/
private BTMetaInfo(Object t_metaInfo) throws ValueException {
// We don't know where we'll save the torrent yet
_completeFile = null;
// Make sure the given object is a Map, and cast it that way
if (!(t_metaInfo instanceof Map)) throw new ValueException("Unknown type of MetaInfo");
Map metaInfo = (Map)t_metaInfo;
/*
* get the trackers, we only expect one tracker, more trackers may be
* added by the addTracker() method, we will throw an exception if the
* tracker is invalid or does not even exist.
*/
// Get the value of the "announce" key
Object t_announce = metaInfo.get("announce"); // t_announce is the value of "accounce", the Web address of the tracker
if (!(t_announce instanceof byte[])) throw new ValueException("bad metainfo - no tracker");
String url = getString((byte[])t_announce); // Convert the ASCII bytes into a String
try {
/*
* Note: this kills UDP trackers so we will eventually use a different object.
*/
// Turn the Web address into a Java URL object, and save that as the only entry in _trackers, an array of URL objects
_trackers = new URL[] { new URL(url) };
} catch (MalformedURLException mue) { throw new ValueException("bad metainfo - bad tracker"); }
/*
* add proper support for multi-tracker torrents later.
*/
/*
* In the .torrent file, there are two main sections:
* "announce" has the address of the tracker
* "info" has the information about the file
*/
// Look up the "info" dictionary item, which has all the information about the files this torrent describes
Object t_info = metaInfo.get("info");
if (!(t_info instanceof Map)) throw new ValueException("bad metainfo - bad info");
Map info = (Map)t_info;
// Within "info", get "pieces", the hashes of all the file pieces
Object t_pieces = info.get("pieces");
if (!(t_pieces instanceof byte[])) throw new ValueException("bad metainfo - no pieces key found");
// Split apart the 20-byte SHA1 hashes, and store them in the _hashes array of 20-byte arrays
_hashes = parsePieces((byte[])t_pieces); // Now, you can look up the hash of piece 0 with _hashes[0]
// Within "info", get "piece length", the size in bytes of the pieces the torrent has broken this file into
Object t_pieceLength = info.get("piece length");
if (!(t_pieceLength instanceof Long)) throw new ValueException("bad metainfo - illegal piece length");
_pieceLength = (int)((Long)t_pieceLength).longValue();
if (_pieceLength <= 0) throw new ValueException("bad metainfo - illegal piece length");
/*
* name of the torrent, also specifying the directory under which to
* save the torrents, as per extension spec, name.urf-8 specifies the
* utf-8 name of the torrent
*/
// Within "info", get "name", the file or folder name we should save this torrent to
String name = null;
Object t_name = info.get("name");
if (!(t_name instanceof byte[])) throw new ValueException("bad metainfo - bad name");
if (info.containsKey("name.utf-8")) { // "info" may also have the key "name.utf-8", the name in UTF-8, which is better than ASCII
try {
name = new String((byte [])info.get("name.utf-8"), Constants.UTF_8_ENCODING);
} catch (UnsupportedEncodingException uee) {}
}
if (name == null) name = getString((byte[])t_name); // Otherwise, it's just regular ASCII
_name = CommonUtils.convertFileName(name); // Replace characters that can't be in a file name to underscore
if (_name.length() == 0) throw new ValueException("bad torrent name"); // Make sure the torrent has a file name
// A .torent can have "files" or "length", but it can't have both, and it can't have neither
if (info.containsKey("files") == info.containsKey("length")) throw new ValueException("single/multiple file mix");
// Make a new Java File object that has the path to the file or folder we'll save this torrent at
_incompleteFile = new File(
SharingSettings.INCOMPLETE_DIRECTORY.getValue(), // The path to LimeWire's temporary folder, like "C:\Documents and Settings\Kevin\Incomplete"
_name); // The folder or file name from the .torrent file, like "File Name.ext" or "Folder Name"
// "info" has the key "files", this .torrent describes a list of files we'll save in a folder
if (info.containsKey("files")) {
// Get the value of "files", which is a list
Object t_files = info.get("files");
if (!(t_files instanceof List)) throw new ValueException("bad metainfo - bad files value");
// Parse "info"."files" into an ArrayList of TorrentFile objects
List files = parseFiles((List)t_files, _incompleteFile.getAbsolutePath());
if (files.size() == 0) throw new ValueException("bad metainfo - bad files value " + t_files);
/*
* files is an ArrayList of TorrentFile objects.
* A TorrentFile object has the path where we'll save a file, and it's length.
* It also has begin and end, the piece numbers where the file starts and finishes.
* These piece numbers aren't in the .torrent file, but we can calculate them.
* We know the piece size, and the file size.
*/
// Calculate the piece numbers where each file starts and ends
long position = 0; // position is our length in bytes into the file, starting at 0, the start
for (Iterator iter = files.iterator(); iter.hasNext(); ) { // Loop for each TorrentFile object we parsed
TorrentFile file = (TorrentFile)iter.next();
// Save the piece number where this file begins and ends
file.begin = (int)(position / _pieceLength);
position += file.LENGTH;
file.end = (int)(position / _pieceLength); // If the file ends on a piece boundary, end will be the next piece
}
// Move the TorrentFile objects into _files
_files = new TorrentFile[files.size()];
files.toArray(_files);
// "info" has the key "length", identifying this .torrent file as a single-file torrent
} else {
// Get the value of "length", the size of the file in bytes
Object t_length = info.get("length");
if (!(t_length instanceof Long)) throw new ValueException("bad metainfo - bad file length");
long length = ((Long)t_length).longValue();
// Setup _files an an array TorentFile objects that only has 1 for the 1 file described here
_files = new TorrentFile[1];
try {
// Make a new TorrentFile object to represent the single file described in this .torrent
_files[0] = new TorrentFile(
length, // The file size
_incompleteFile.getCanonicalPath()); // The path to LimeWire's temporary folder, like "C:\Documents and Settings\Kevin\Incomplete"
// The file starts in piece 0
_files[0].begin = 0;
// Set end to the number of pieces
_files[0].end = _hashes.length;
// getCanonicalPath() threw an exception
} catch (IOException ie) { throw new ValueException("bad metainfo - file path"); }
}
/*
* The info hash is the hash of the "info" dictionary in a .torrent file.
* BitTorrent programs use the info hash to identify the torrent.
* The info hash isn't in the .torrent, because any program that has the .torrent can calculate it for itself.
*/
/*
* create the info hash, we could create the info hash while reading it
* but that would make the code a lot more complex. This works well too,
* because the order of a list is not changed during the process of
* decoding or encoding it and Maps are always sorted alphanumerically
* when encoded.
* So the data we encoded is always exactly the same as the data before
* we decoded it. This is intended that way by the protocol.
*/
// Turn info, the Java HashMap we parsed the "info" dictionary into, back into bencoded data
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // Make a ByteArrayOutputStream that will grow as we add data to it
try {
BEncoder.encodeDict(baos, info);
} catch (IOException ioe) { ErrorService.error(ioe); }
// Compute the SHA1 hash of the bencoded data
MessageDigest md = new SHA1(); // LimeWire's SHA1 class extend's Java's MessageDigest class
_infoHash = md.digest(baos.toByteArray());
// Turn it into a URN object that holds text like "urn:sha1:JAZSGOLT6UP4I5N5KGJRZPSF6RZCEJKQ"
try {
_infoHashURN = URN.createSHA1UrnFromBytes(_infoHash);
} catch (IOException impossible) { ErrorService.error(impossible); }
// Save the parsed "info" HashMap as _infoMap, and make sure no one else can change it
_infoMap = Collections.unmodifiableMap(info);
// Add up the length of all the files, and save the total size
_totalSize = calculateTotalSize(_files);
// Make a new VerifyingFolder object from this BTMetaInfo one
initializeVerifyingFolder(null, false); // false, we're not done downloading this torrent yet
}
/**
* Convert a byte array of ASCII text characters into a Java String object.
*
* @param bytes A byte array that contains ASCII text.
* @return The text converted to a Java String object.
* null on error.
*/
private static String getString(byte[] bytes) {
try {
// Make a new String, using the default ASCII encoding
return new String(bytes, Constants.ASCII_ENCODING);
} catch (UnsupportedEncodingException impossible) {
// Java couldn't make that conversion, return null instead.
ErrorService.error(impossible);
return null;
}
}
/**
* Save this BTMetaInfo object to data.
*
* @param out An ObjectOutputStream object we can call writeObject() on to give it objects
*/
private synchronized void writeObject(ObjectOutputStream out) throws IOException {
// List all the parts of this BTMetaInfo object that we want to save in a HashMap
Map toWrite = new HashMap();
toWrite.put("_hashes", _hashes);
toWrite.put("_pieceLength", new Integer(_pieceLength));
toWrite.put("_name", _name);
toWrite.put("_files", _files);
toWrite.put("_completeFile", _completeFile);
toWrite.put("_incompleteFile", _incompleteFile);
toWrite.put("_infoMap", _infoMap);
toWrite.put("_infoHash", _infoHash);
toWrite.put("_trackers", _trackers);
toWrite.put("_totalSize", new Long(_totalSize));
toWrite.put("folder data", _folder.getSerializableObject());
// Serialize the HashMap to the given ObjectOutputStream
out.writeObject(toWrite);
}
/**
* Read data to make all the parts of this BTMetaInfo object.
*
* @param in An ObjectInputStream object we can call readObject() on to get objects from
*/
private synchronized void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Read the HashMap we made and saved with all the other objects inside
Map toRead = (Map)in.readObject();
// Get each unserialized object out of it, and save it in the member variables of this BTMetaInfo object
_hashes = (byte[][])toRead.get("_hashes");
Integer pieceLength = (Integer)toRead.get("_pieceLength");
_name = (String)toRead.get("_name");
_files = (TorrentFile[])toRead.get("_files");
_incompleteFile = (File)toRead.get("_incompleteFile");
_completeFile = (File)toRead.get("_completeFile");
_infoMap = (Map)toRead.get("_infoMap");
_infoHash = (byte[])toRead.get("_infoHash");
_trackers = (URL[])toRead.get("_trackers");
Long totalSize = (Long)toRead.get("_totalSize");
Map folderData = (Map)toRead.get("folder data");
// Make sure we got everything
if (_hashes == null ||
pieceLength == null ||
_name == null ||
_files == null ||
_incompleteFile == null ||
_completeFile == null ||
_infoMap == null ||
_infoHash == null ||
_trackers == null ||
totalSize == null ||
folderData == null)
throw new IOException("cannot read BTMetaInfo");
// Save numbers
_pieceLength = pieceLength.intValue();
_totalSize = totalSize.longValue();
// Make the VerifyingFolder object from this BTMetaInfo one
initializeVerifyingFolder(folderData, false); // false, we're not done downloading this torrent yet
}
/**
* Parse "info"."files" into an ArrayList of TorrentFile objects.
*
* In a multifile .torrent, "info"."files" is a list.
* Each item in the list has the save path like "Folder\Folder\Name.ext" and size of a file.
* parseFiles() turns each list item into a TorrentFile object, and returns an ArrayList of them.
* The paths in the TorentFile objects are complete with the path to LimeWire's Incomplete folder and the "Torrent Name" folder.
*
* @param files A Java ArrayList with the Java objects we made from reading the "info"."files" part of the .torrent.
* @param basePath The path to LimeWire's temporary folder, and the folder name from "info"."name".
* Like "C:\Documents and Settings\Kevin\Incomplete\Folder Name".
* @return An ArrayList of TorrentFile objects.
* Each TorrentFile object has information about a file listed in the .torrent.
* It has the path to where we'll save it on disk, like "C:\Documents and Settings\Kevin\Incomplete\Torrent Name\Folder\Folder\File.ext".
* It also has the file size.
*/
private static List parseFiles(List files, String basePath) throws ValueException {
// Make an empty ArrayList for us to fill and return
ArrayList ret = new ArrayList();
// Loop for each item in the "files" list
for (Iterator iter = files.iterator(); iter.hasNext(); ) {
Object t_file = iter.next();
if (!(t_file instanceof Map)) throw new ValueException("bad metainfo - bad file value");
// Parse the file's path and size, wrap those in a TorrentFile object, and add it to ret
ret.add(parseFile((Map)t_file, basePath));
}
// Return the ArrayList of TorrentFile objects we made
return ret;
}
/**
* Compose the save path and get the file size of a single file listed in a multifile .torrent.
*
* In a multifile .torrent, "info"."files" is a list.
* Each item in the list is a dictionary with keys and values like this:
*
* length 521859
* path "Folder" "Subfolder" "Another Subfolder" "File Name.ext"
* path.utf-8 "Folder" "Subfolder" "Another Subfolder" "File Name.ext"
*
* length is a bencoded number, while path and path.utf-8 are bencoded lists.
* length is the size of the file
* path is the subfolders you should save it in, like "Folder\Subfolder\Another Subfolder\File Name.ext".
* In the .torrent, "info"."name" contains the folder name for this torrent, like "Folder Name".
* LimeWire settings have the path to the temporary save folder, like "C:\Documents and Settings\Kevin\Incomplete".
* From all this, we can put together the complete path to the file, like:
* "C:\Documents and Settings\Kevin\Incomplete\Folder Name\Folder\Subfolder\Another Subfolder\File Name.ext"
*
* Both "path" and "path.utf-8" should contain the same information.
* All .torrent files will have "path", while some will also have "path.utf-8".
* "path" is encoded in regular ASCII, while "path.utf-8" is in UTF-8, allowing non English-language characters.
* If we have "path.utf-8", this parseFile() method uses it instead of "path".
*
* @param file The Java HashMap we made by parsing a file item in the "info"."files" list in the .torrent file.
* @param basePath The path to LimeWire's temporary folder, and the folder name from "info"."name".
* Like "C:\Documents and Settings\Kevin\Incomplete\Folder Name".
* @return A TorrentFile object with the complete save path for the file, and its size.
*/
private static TorrentFile parseFile(Map file, String basePath) throws ValueException {
// Read "length", the file size
Object t_length = file.get("length");
if (!(t_length instanceof Long)) throw new ValueException("bad metainfo - bad file length");
long length = ((Long)t_length).longValue();
// Read "path", the path and file name in a bencoded list of strings, like "Folder", "Subfolder", "File.ext"
Object t_path = file.get("path");
if (!(t_path instanceof List)) throw new ValueException("bad metainfo - bad path");
List path = (List)t_path;
if (path.isEmpty()) throw new ValueException("bad metainfo - bad path");
/*
* The value of "path" is a bencoded list of strings, like:
*
* "Folder" "Subfolder" "File Name.ext"
*
* You can put these together to make the end of the path, like "Folder\Subfolder\File Name.ext".
*
* The .torrent file may have another key alongside "path", named "path.utf-8".
* It has the same path, but encoded in UTF-8 instead of regular ASCII.
* A .torrent file that has "path.utf-8" also has the same information in "path" so BitTorrent programs that don't understand "path.utf-8" can still understand it.
* We do understand it, though, so we'll use it instead of "path" if it's there.
*/
// If the key "path.utf-8" is present, the path is written in UTF-8 in addition to ASCII
Object t_path_utf8 = file.get("path.utf-8");
if (!(t_path_utf8 instanceof List)) t_path_utf8 = null; // "path.utf-8" not found, set t_path_utf8 to null to use "path" instead
// Make a new StringBuffer that we can add text to
StringBuffer paths = new StringBuffer(basePath); // Start it out with the path to the folder we're going to save this torrent in
// Count the number of paths we parse
int numParsed = 0;
// The .torrent has the path in UTF-8
if (t_path_utf8 != null) {
List pathUtf8 = (List)t_path_utf8;
// Only take it if it has the same number of folders as the ASCII path in "path"
if (pathUtf8.size() == path.size()) {
// Loop for each folder name in the "path.utf-8" bencoded list of bencoded strings
for (Iterator iter = pathUtf8.iterator(); iter.hasNext(); ) {
Object t_next = iter.next();
if (!(t_next instanceof byte[])) break; // Leave the loop to use "path" instead
// Make a string to hold this folder name
String pathElement;
try {
// Read the bencoded string using UTF-8 encoding
pathElement = new String((byte[])t_next, Constants.UTF_8_ENCODING);
// Add "\" and the folder name to the paths string we're building
paths.append(File.separator);
paths.append(CommonUtils.convertFileName(pathElement));
// Count that we parsed another path element
numParsed++;
// Leave the loop to use "path" instead
} catch (UnsupportedEncodingException uee) { break; }
}
}
}
// If the loop above didn't find all the elements that "path" has, use it instead
if (numParsed < path.size()) {
// Loop for each string in "path"
for (int i = numParsed; i < path.size(); i++) {
Object next = path.get(i);
if (!(next instanceof byte[])) throw new ValueException("bad paths");
String pathElement = getString((byte[])next);
// Add "\" and the path element to paths
paths.append(File.separator);
paths.append(CommonUtils.convertFileName(pathElement));
}
}
/*
* Now, paths is like this:
* "C:\Documents and Settings\Kevin\Incomplete\Folder\Subfolder\File Name.ext"
*/
// Store the complete path we composed and the length of the file in a new TorrentFile object, and return it
return new TorrentFile(length, paths.toString());
}
/**
* Split the pieces byte array of hashes into individual hashes.
*
* @param pieces A byte array with the SHA1 hashes of all the pieces, one after the other with nothing between them
* @return A 2-D array with each 20-byte SHA1 hash presented seprately
*/
private static byte[][] parsePieces(byte[] pieces) throws ValueException {
// Make sure the given array is exactly a multiple of 20 bytes long
if (pieces.length % 20 != 0) throw new ValueException("bad metainfo - bad pieces key");
// Allocate a 2-D byte array exactly the right size
byte[][] ret = new byte[pieces.length / 20][20];
// Move i down the given array in 20-byte steps
int k = 0; // The index in our array we're filling
for (int i = 0; i < pieces.length; i += 20) {
// Copy the next 20-byte SHA1 hash from pieces into the next space in ret
System.arraycopy(pieces, i, ret[k++], 0, 20); // ret[k++] returns ret[k], and then increments the value of k for the next time
}
// Return the array we made
return ret;
}
/**
* A FakeFileDesc object holds a path like "C:\Documents and Settings\Kevin\Incomplete\Folder\Subfolder\File Name.ext" that the GUI can read.
* LimeWire's GUI needs a FileDesc object to be able to show a file in its list.
* FakeFileDesc extends FileDesc to fill this requirement.
*/
public class FakeFileDesc extends FileDesc {
/**
* Make a new FakeFileDesc object.
* A FakeFileDesc object holds a path, and satisifies the minimum requirements of being a FileDesc object.
* The GUI needs a FileDesc object to list a file, and will be able to get the path from this FakeFileDesc object.
*
* @param file A Java File object that has the path this FakeFileDesc object will hold
*/
public FakeFileDesc(File file) {
// Call the FileDesc constructor
super(
file, // The path for the GUI to read
FAKE_URN_SET, // The FileDesc constructor needs a Set of URN objects, give it the one we made with a single URN that has a hash that's all 0s
Integer.MAX_VALUE); // Instead of a real file index, pass the largest int
}
}
}