/*
* Copyright 2006 Robert Sterling Moore II
*
* This computer program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option) any
* later version.
*
* This computer program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this computer program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
/**
* Unencodes a .torrent file and stores its data in a TorrentFile object.
*
* @author Robert S. Moore II
*/
public class TorrentFileHandler
{
/*
* This class is used because Java passes by value for primitive data types,
* but passes by reference for Objects. Passing this object instead of a
* simple int allows methods to update the value of the index without using
* global variables.
*/
private class Index
{
public int index;
public Index()
{
super();
this.index = 0;
}
}
// Used to determine the next bencoded data type in the file.
private final int NULL_TYPE = 0;
private final int STRING = 1;
private final int INTEGER = 2;
private final int LIST = 3;
private final int DICTIONARY = 4;
private final int STRUCTURE_END = 5;
// Stores the unencoded data.
private TorrentFile torrent_file;
//Unbencodes data
private Bencoder bencoder;
/**
* Constructs a new TorrentFileHandler object
*
*/
public TorrentFileHandler()
{
super();
this.torrent_file = new TorrentFile();
this.bencoder = new Bencoder();
}
public TorrentFile openTorrentFile(String file_name)
{
byte[] file_data = getBytesFromFile(file_name);
HashMap file_data_map;
Index index = new Index();
file_data_map = parseDictionary(file_data, index);
if(!storeDataInTorrent(file_data_map))
{
return null;
}
return this.torrent_file;
}
private byte[] getBytesFromFile(String file_name)
{
File file = new File(file_name);
long file_size_long = -1;
byte[] file_bytes = null;
InputStream file_stream;
try
{
file_stream = new FileInputStream(file);
// Verify that the file exists
if (!file.exists())
{
System.err
.println("Error: [TorrentFileHandler.java] The file \""
+ file_name
+ "\" does not exist. Please make sure you have the correct path to the file.");
return null;
}
// Verify that the file is readable
if (!file.canRead())
{
System.err
.println("Error: [TorrentFileHandler.java] Cannot read from \""
+ file_name
+ "\". Please make sure the file permissions are set correctly.");
return null;
}
// The following code was derived from
// http://javaalmanac.com/egs/java.io/File2ByteArray.html
file_size_long = file.length();
// Avoid overflow in the file length
if (file_size_long > Integer.MAX_VALUE)
{
System.err.println("Error: [TorrentFileHandler.java] The file \"" + file_name
+ "\" is too large to be read by this class.");
return null;
}
// Initialize the byte array for the file's data
file_bytes = new byte[(int) file_size_long];
int file_offset = 0;
int bytes_read = 0;
// Read from the file
while (file_offset < file_bytes.length
&& (bytes_read = file_stream.read(file_bytes, file_offset,
file_bytes.length - file_offset)) >= 0)
{
file_offset += bytes_read;
}
// Verify that we read everything from the file
if (file_offset < file_bytes.length)
{
throw new IOException("Could not completely read file \""
+ file.getName() + "\".");
}
// End of code from
// http://javaalmanac.com/egs/java.io/File2ByteArray.html
file_stream.close();
}
catch (FileNotFoundException e)
{
System.err
.println("Error: [TorrentFileHandler.java] The file \""
+ file_name
+ "\" does not exist. Please make sure you have the correct path to the file.");
return null;
}
catch (IOException e)
{
System.err
.println("Error: [TorrentFileHandler.java] There was a general, unrecoverable I/O error while reading from \""
+ file_name + "\".");
System.err.println(e.getMessage());
}
return file_bytes;
}
/**
* Reads the byte at data[index.index] and returns an integer
* based on the value.
*
* @param data
* Contains bencoded data.
* @param index
* A valid index into data that points to the beginning of a
* bencoded String, Integer, List or Dictionary.
* @return An int based on the value of the byte at
* data[index.index].
*/
private int getEncodedType(byte[] data, Index index)
{
// The value to be returned
int return_value = NULL_TYPE;
// Set return_value according to the byte at data[index.index]
switch ((char) data[index.index])
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return_value = STRING;
break;
case 'i':
return_value = INTEGER;
break;
case 'l':
return_value = LIST;
break;
case 'd':
return_value = DICTIONARY;
break;
case 'e':
return_value = STRUCTURE_END;
break;
default:
System.err
.println("Error: [TorrentFileHandler.java] The byte at position "
+ index.index
+ " in the .torrent file is not the beginning of a bencoded data type.");
break;
}
return return_value;
}
/**
* Parses a bencoded String located at data[index.index] and
* returns it as a String object. After being called,
* index.index points to the byte after the end of the String
* (the next data structure).
*
* @param data
* Contains bencoded data.
* @param index
* A valid index into data that points to the
* beginning of a bencoded String.
* @return A String representing the bencoded String at
* data[index.index].
*/
private String parseString(byte[] data, Index index)
{
String return_string = null;
int temp_index = index.index;
int power_of_ten = 1;
int length_of_string = 0;
boolean first_digit = false;
StringBuffer temp_string = new StringBuffer();
// Determine the length of the integer representing the String's length.
while (data[temp_index] != (byte) ':')
{
if (first_digit)
{
power_of_ten *= 10;
}
first_digit = true;
temp_index++;
}
// Determine the length of the string.
while (data[index.index] != (byte) ':')
{
length_of_string += ((data[index.index] - 48) * power_of_ten);
power_of_ten /= 10;
index.index++;
}
// Skip the ':'
index.index++;
// Extract the string.
while ((length_of_string > 0) && (index.index <= data.length))
{
temp_string.append((char) data[index.index]);
length_of_string--;
index.index++;
}
return_string = temp_string.toString();
return return_string;
}
/**
* Parses a bencoded Integer located at data[index.index] and
* returns it as an Integer object. After being called,
* index.index points to the byte after the end of the
* Integer (the next data structure).
*
* @param data
* Contains bencoded data.
* @param index
* A valid index into data that points to the
* beginning of a bencoded Integer.
* @return An Integer representing the bencoded Integer at
* data[index.index].
*/
private Integer parseInteger(byte[] data, Index index)
{
Integer return_integer;
int temp_value = 0;
int power_of_ten = 1;
boolean first_digit = false;
boolean is_negative = false;
// Skip the 'i'
index.index++;
if(data[index.index] == (byte)'-')
{
is_negative = true;
index.index++;
}
int temp_index = index.index;
// Determine the length of the integer representing the String's length.
while (data[temp_index] != (byte) 'e')
{
if (first_digit)
{
power_of_ten *= 10;
}
first_digit = true;
temp_index++;
}
// Determine the length of the string.
while (data[index.index] != (byte) 'e')
{
temp_value += ((data[index.index] - 48) * power_of_ten);
power_of_ten /= 10;
index.index++;
}
// Skip the 'e'
index.index++;
if(is_negative)
{
return_integer = new Integer(-temp_value);
}
else
{
return_integer = new Integer(temp_value);
}
return return_integer;
}
/**
* Parses a bencoded List located data[index.index] and
* returns it as a List object. After being called, index.index
* points to the byte after the end of the List (the next data structure).
*
* @param data
* Contains bencoded data.
* @param index
* A valid index into data that points to the
* beginning of a bencoded List.
* @return A List representing the bencoded List at
* data[index.index].
*/
private Vector parseList(byte[] data, Index index)
{
Vector return_list = new Vector();
// Skip the 'l'
index.index++;
int next_data_type = getEncodedType(data, index);
while ((next_data_type != STRUCTURE_END)
&& (next_data_type != NULL_TYPE) && (index.index < data.length))
{
switch(next_data_type)
{
case INTEGER:
return_list.add(parseInteger(data, index));
break;
case STRING:
return_list.add(parseString(data, index));
break;
case LIST:
return_list.add(parseList(data, index));
break;
case DICTIONARY:
return_list.add(parseDictionary(data, index));
break;
default:
System.err.println("Error: [TorrentFileHandler.java] The object at position " + index.index
+ " is not a valid bencoded data type.");
return null;
}
next_data_type = getEncodedType(data, index);
}
//Skip the 'e'
index.index++;
return return_list;
}
/**
* Parses a bencoded Dictionary located data[index.index] and
* returns it as a Map object. After being called, index.index
* points to the byte after the end of the Dictionary (the next data
* structure).
*
* @param data
* Contains bencoded data.
* @param index
* A valid index into data that points to the
* beginning of a bencoded Dictionary.
* @return A Map representing the bencoded Dictionary at
* data[index.index].
*/
private HashMap parseDictionary(byte[] data, Index index)
{
HashMap returned_map = new HashMap(10);
String key;
Object value;
// Skip the 'd'
index.index++;
int next_data_type = getEncodedType(data, index);
// As long as there isn't an error or the end of our dictionary, keep
// parsing the entries.
while ((next_data_type != NULL_TYPE)
&& (next_data_type != STRUCTURE_END)
&& (index.index < data.length))
{
// The key is ALWAYS a string.
if (next_data_type != STRING)
{
System.err
.println("Error: [TorrentFileHandler.java] The bencoded object beginning at index "
+ index.index
+ " is not a String, but must be according to the BitTorrent definition.");
}
key = parseString(data, index);
// Now get the data type of the value
next_data_type = getEncodedType(data, index);
switch (next_data_type)
{
case INTEGER:
value = parseInteger(data, index);
break;
case STRING:
value = parseString(data, index);
break;
case LIST:
value = parseList(data, index);
break;
case DICTIONARY:
if(key.equalsIgnoreCase("info"))
{
int old_index = index.index;
value = parseDictionary(data, index);
byte[] info = new byte[index.index-old_index];
for(int i = 0; i < info.length; i++)
{
info[i] = data[old_index + i];
}
torrent_file.info_hash_as_binary = generateSHA1Hash(info);
torrent_file.info_hash_as_url = byteArrayToURLString(torrent_file.info_hash_as_binary);
torrent_file.info_hash_as_hex = byteArrayToByteString(torrent_file.info_hash_as_binary);
}
else
{
value = parseDictionary(data, index);
}
break;
default:
System.err.println("Error: [TorrentFileHandler.java] The value of the key \"" + key
+ "\" is not a valid bencoded data type.");
return null;
}
returned_map.put(key, value);
//System.out.println("[" + key + "/" + value.toString() + "]");
next_data_type = getEncodedType(data, index);
}
//Skip the 'e'
index.index++;
return returned_map;
}
private boolean storeDataInTorrent(Map torrent_data_map)
{
Map info_map = (Map)torrent_data_map.get("info");
if(info_map == null)
{
System.err.println("Error: [TorrentFileHandler.java] Could not retrieve the info dictionary.");
return false;
}
if(!getPieceHashes((String)info_map.get("pieces")))
{
return false;
}
torrent_file.tracker_url = (String)torrent_data_map.get("announce");
if(torrent_file.tracker_url == null)
{
System.err.println("Error: [TorrentFileHandler.java] Could not retrieve the tracker URL.");
return false;
}
torrent_file.file_length = ((Integer)info_map.get("length")).intValue();
if(torrent_file.file_length < 0)
{
System.err.println("Error: [TorrentFileHandler.java] Could not retrieve the file length.");
return false;
}
torrent_file.piece_length = ((Integer)info_map.get("piece length")).intValue();
if(torrent_file.piece_length < 0)
{
System.err.println("Error: [TorrentFileHandler.java] Could not retrieve the piece length.");
return false;
}
return true;
}
private boolean getPieceHashes(String hash_string)
{
if(hash_string.length() % 20 != 0)
{
System.err.println("Error: [TorrentFileHandler.java] The SHA-1 hash for the file's pieces is not the correct length.");
return false;
}
byte[] binary_data = new byte[hash_string.length()];
byte[] individual_hash;
int number_of_pieces = binary_data.length / 20;
for(int i = 0; i < binary_data.length; i++)
{
binary_data[i] = (byte)hash_string.charAt(i);
}
for(int i = 0; i < number_of_pieces; i++)
{
individual_hash = new byte[20];
for(int j = 0; j < 20; j++)
{
individual_hash[j] = binary_data[(20*i)+j];
}
torrent_file.piece_hash_values_as_binary.add(individual_hash);
torrent_file.piece_hash_values_as_hex.add(byteArrayToByteString(individual_hash));
torrent_file.piece_hash_values_as_url.add(byteArrayToURLString(individual_hash));
}
return true;
}
/*
* Stolen from byteArrayToByteString
*/
private static String byteArrayToURLString(byte in[])
{
byte ch = 0x00;
int i = 0;
if (in == null || in.length <= 0)
return null;
String pseudo[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F" };
StringBuffer out = new StringBuffer(in.length * 2);
while (i < in.length)
{
// First check to see if we need ASCII or HEX
if ((in[i] >= '0' && in[i] <= '9')
|| (in[i] >= 'a' && in[i] <= 'z')
|| (in[i] >= 'A' && in[i] <= 'Z') || in[i] == '$'
|| in[i] == '-' || in[i] == '_' || in[i] == '.'
|| in[i] == '+' || in[i] == '!')
{
out.append((char) in[i]);
i++;
}
else
{
out.append('%');
ch = (byte) (in[i] & 0xF0); // Strip off high nibble
ch = (byte) (ch >>> 4); // shift the bits down
ch = (byte) (ch & 0x0F); // must do this is high order bit is
// on!
out.append(pseudo[(int) ch]); // convert the nibble to a
// String Character
ch = (byte) (in[i] & 0x0F); // Strip off low nibble
out.append(pseudo[(int) ch]); // convert the nibble to a
// String Character
i++;
}
}
String rslt = new String(out);
return rslt;
}
/**
*
* Convert a byte[] array to readable string format. This makes the "hex"
* readable!
*
* @author Jeff Boyle
*
* @return result String buffer in String format
*
* @param in
* byte[] buffer to convert to string format
*
*/
// Taken from http://www.devx.com/tips/Tip/13540
private static String byteArrayToByteString(byte in[])
{
byte ch = 0x00;
int i = 0;
if (in == null || in.length <= 0)
return null;
String pseudo[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F" };
StringBuffer out = new StringBuffer(in.length * 2);
while (i < in.length)
{
ch = (byte) (in[i] & 0xF0); // Strip off high nibble
ch = (byte) (ch >>> 4); // shift the bits down
ch = (byte) (ch & 0x0F); // must do this is high order bit is on!
out.append(pseudo[(int) ch]); // convert the nibble to a String
// Character
ch = (byte) (in[i] & 0x0F); // Strip off low nibble
out.append(pseudo[(int) ch]); // convert the nibble to a String
// Character
i++;
}
String rslt = new String(out);
return rslt;
}
private byte[] generateSHA1Hash(byte[] bytes)
{
try
{
byte[] hash = new byte[20];
MessageDigest sha = MessageDigest.getInstance("SHA-1");
hash = sha.digest(bytes);
return hash;
}
catch (NoSuchAlgorithmException e)
{
System.err
.println("Error: [TorrentFileHandler.java] \"SHA-1\" is not a valid algorithm name.");
System.exit(1);
}
return null;
}
}