/* * 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 <code>data[index.index]</code> 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 <code>int</code> based on the value of the byte at * <code>data[index.index]</code>. */ 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 <code>data[index.index]</code> and * returns it as a String object. After being called, * <code>index.index</code> 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 <code>data</code> that points to the * beginning of a bencoded String. * @return A String representing the bencoded String at * <code>data[index.index]</code>. */ 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 <code>data[index.index]</code> and * returns it as an Integer object. After being called, * <code>index.index</code> 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 <code>data</code> that points to the * beginning of a bencoded Integer. * @return An Integer representing the bencoded Integer at * <code>data[index.index]</code>. */ 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 <code>data[index.index]</code> and * returns it as a List object. After being called, <code>index.index</code> * 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 <code>data</code> that points to the * beginning of a bencoded List. * @return A List representing the bencoded List at * <code>data[index.index]</code>. */ 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 <code>data[index.index]</code> and * returns it as a Map object. After being called, <code>index.index</code> * 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 <code>data</code> that points to the * beginning of a bencoded Dictionary. * @return A Map representing the bencoded Dictionary at * <code>data[index.index]</code>. */ 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; } }