/*
* RUBTClient is a BitTorrent client written at Rutgers University for
* instructional use.
* Copyright (C) 2008-2009 Robert Moore II
*
* This 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 3 of the License, or
* (at your option) any later version.
*
* This 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 program. If not, see <http://www.gnu.org/licenses/>.
*/
package GivenTools;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeMap;
/**
* Parses a Bencoded byte array and returns a combination of {@code Map},
* {@code List}, {@code ByteBuffer}, and {@code Integer} objects.
*
* @author Robert Moore II
*
*/
public final class Bencoder2
{
/**
* Indicates an invalid object type or bencoded object.
*/
private static final int INVALID = -1;
/**
* Indicates that an object is a dictionary.
*/
private static final int DICTIONARY = 0;
/**
* Indicates that an object is an integer.
*/
private static final int INTEGER = 1;
/**
* Indicates an object is a byte string.
*/
private static final int STRING = 2;
/**
* Indicates an object is a list.
*/
private static final int LIST = 3;
/*
********************************************
************ CONVENIENCE METHODS ***********
********************************************
*/
/**
* Extracts the bencoded 'info' dictionary from a metainfo torrent file.
* @param torrent_file_bytes the bencoded metainfo dictionary.
* @return a {@code ByteBuffer} containing the bencoded 'info' dictionary from the metainfo file.
* @throws BencodingException if the 'info' key is not contained in the decoded dictionary.
*/
public static final ByteBuffer getInfoBytes(byte[] torrent_file_bytes) throws BencodingException
{
Object[] vals = decodeDictionary(torrent_file_bytes,0);
if(vals.length != 3 || vals[2] == null)
throw new BencodingException("Exception: No info bytes found!");
return (ByteBuffer)vals[2];
}
/*
********************************************
************ BDECODING METHODS *************
********************************************
*/
/**
* Decodes a bencoded object represented by the byte array.
* @param bencoded_bytes the bencoded data to decode.
* @return either a {@code Map}, {@code List}, {@code ByteBuffer}, or {@code Integer}.
* @throws BencodingException if the bencoded data was improperly formatted.
*/
public static final Object decode(byte[] bencoded_bytes) throws BencodingException
{
return decode(bencoded_bytes, 0)[1];
}
/**
* Decodes a bencoded object represented by the byte array, starting at the specified offset.
* @param bencoded_bytes the bencoded data to decode.
* @param offset the offset into {@code bencoded_bytes} at which to start decoding.
* @return a {@code Map}, {@code List}, {@code ByteBuffer}, or {@code Integer}.
* @throws BencodingException if the bencoded object in {@code bencoded_bytes} at offset {@code offset} is incorrectly encoded.
*/
private static final Object[] decode(byte[] bencoded_bytes, int offset) throws BencodingException
{
switch(nextObject(bencoded_bytes, offset))
{
case DICTIONARY:
return decodeDictionary(bencoded_bytes, offset);
case LIST:
return decodeList(bencoded_bytes, offset);
case INTEGER:
return decodeInteger(bencoded_bytes, offset);
case STRING:
return decodeString(bencoded_bytes, offset);
default:
return null;
}
}
/**
* Decodes an integer from the byte array.
* @param bencoded_bytes the byte array of the bencoded integer.
* @param offset the position of the 'i' indicating the start of the
* bencoded integer to be bdecoded.
* @return an <code>Object[]</code> containing an <code>Integer</code> offset and the decoded
* <code>Integer</code>, in positions 0 and 1, respectively
* @throws BencodingException if the bencoded integer in {@code bencoded_bytes} at offset {@code offset} is incorrectly encoded.
*/
private static final Object[] decodeInteger(byte[] bencoded_bytes, int offset) throws BencodingException
{
StringBuffer int_chars = new StringBuffer();
offset++;
for(; bencoded_bytes[offset] != (byte)'e' && bencoded_bytes.length > (offset); offset++)
{
if((bencoded_bytes[offset] < 48 || bencoded_bytes[offset] > 57) && bencoded_bytes[offset] != 45)
throw new BencodingException("Expected an ASCII integer character, found " + (int)bencoded_bytes[offset]);
int_chars.append((char)bencoded_bytes[offset]);
}
try
{
offset++; // Skip the 'e'
return new Object[] {new Integer(offset),new Integer(Integer.parseInt(int_chars.toString()))};
}
catch(NumberFormatException nfe)
{
throw new BencodingException("Could not parse integer at position" + offset + ".\nInvalid character at position " + offset + ".");
}
}
/**
* Decodes a byte string from the byte array
* @param bencoded_bytes the bencoded form of the byte string.
* @param offset the offset into {@code bencoded_bytes} where the byte string begins.
* @return an <code>Object[]</code> containing an <code>Integer</code> offset and the decoded
* byte string (as a {@code ByteBuffer}), in positions 0 and 1, respectively
* @throws BencodingException if the bencoded object is incorrectly encoded.
*/
private static final Object[] decodeString(byte[] bencoded_bytes, int offset) throws BencodingException
{
StringBuffer digits = new StringBuffer();
while(bencoded_bytes[offset] > '/' && bencoded_bytes[offset] < ':')
{
digits.append((char)bencoded_bytes[offset++]);
}
if(bencoded_bytes[offset] != ':')
{
throw new BencodingException("Error: Invalid character at position " + offset + ".\nExpecting ':' but found '" + (char)bencoded_bytes[offset] + "'.");
}
offset++;
int length = Integer.parseInt(digits.toString());
byte[] byte_string = new byte[length];
System.arraycopy(bencoded_bytes, offset, byte_string, 0, byte_string.length);
return new Object[] {new Integer(offset+length), ByteBuffer.wrap(byte_string)};
}
/**
* Decodes a list from the bencoded byte array.
* @param bencoded_bytes the bencoded form of the list.
* @param offset the offset into {@code bencoded_bytes} where the list begins.
* @return an <code>Object[]</code> containing an <code>Integer</code> offset and the decoded
* list (as a {@code List}), in positions 0 and 1, respectively
* @throws BencodingException if the bencoded object is incorrectly encoded.
*/
@SuppressWarnings("unchecked")
private static final Object[] decodeList(byte[] bencoded_bytes, int offset) throws BencodingException
{
ArrayList list = new ArrayList();
offset++;
Object[] vals;
while(bencoded_bytes[offset] != (byte)'e')
{
vals = decode(bencoded_bytes,offset);
offset = ((Integer)vals[0]).intValue();
list.add(vals[1]);
}
offset++;
return new Object[] {new Integer(offset), list};
}
/**
* Decodes a dictionary from the bencoded byte array.
* @param bencoded_bytes the bencoded form of the dictionary.
* @param offset the offset into {@code bencoded_bytes} where the dictionary begins.
* @return an <code>Object[]</code> containing an <code>Integer</code> offset and the decoded
* dictionary (as a {@code Map}, in positions 0 and 1, respectively
* @throws BencodingException if the bencoded object is incorrectly encoded.
*/
@SuppressWarnings("unchecked")
private static final Object[] decodeDictionary(byte[] bencoded_bytes, int offset) throws BencodingException
{
HashMap map = new HashMap();
++offset;
ByteBuffer info_hash_bytes = null;
while(bencoded_bytes[offset] != (byte)'e')
{
// Decode the key, which must be a byte string
Object[] vals = decodeString(bencoded_bytes, offset);
ByteBuffer key = (ByteBuffer)vals[1];
offset = ((Integer)vals[0]).intValue();
boolean match = true;
for(int i = 0; i < key.array().length && i < 4; i++)
{
if(!key.equals(ByteBuffer.wrap(new byte[]{'i', 'n','f','o'})))
{
match = false;
break;
}
}
int info_offset = -1;
if(match)
info_offset = offset;
vals = decode(bencoded_bytes, offset);
offset = ((Integer)vals[0]).intValue();
if(match)
{
info_hash_bytes = ByteBuffer.wrap(new byte[offset - info_offset]);
info_hash_bytes.put(bencoded_bytes,info_offset, info_hash_bytes.array().length);
}
else if(vals[1] instanceof HashMap)
{
info_hash_bytes = (ByteBuffer)vals[2];
}
if(vals[1] != null)
map.put(key,vals[1]);
}
return new Object[] {new Integer(++offset), map, info_hash_bytes};
}
/**
* Determines the bencoded data type at {@code bencoded_bytes[offset]}.
* @param bencoded_bytes the bencoded data.
* @param offset the offset into {@code bencoded_bytes} that contains a bencoded object.
* @return the type of the bencoded object.
* @see #DICTIONARY
* @see #LIST
* @see #INTEGER
* @see #STRING
* @see #INVALID
*/
private static final int nextObject(byte[] bencoded_bytes, int offset)
{
switch(bencoded_bytes[offset])
{
case (byte)'d':
return DICTIONARY;
case (byte)'i':
return INTEGER;
case (byte)'l':
return LIST;
case (byte)'0':
case (byte)'1':
case (byte)'2':
case (byte)'3':
case (byte)'4':
case (byte)'5':
case (byte)'6':
case (byte)'7':
case (byte)'8':
case (byte)'9':
return STRING;
default:
return INVALID;
}
}
/*
********************************************
************ BENCODING METHODS *************
********************************************
*/
/**
* Bencodes the specified object as a {@code byte[]}.
* @param o the object to bencode.
* @return the bencoded form of the object.
* @throws BencodingException if {@code o} is not of type {@code HashMap}, {@code ArrayList},
* {@code Integer}, or {@code ByteBuffer}.
*/
@SuppressWarnings("unchecked")
public static final byte[] encode(Object o) throws BencodingException
{
if(o instanceof HashMap)
return encodeDictionary((HashMap)o);
else if(o instanceof ArrayList)
return encodeList((ArrayList)o);
else if(o instanceof Integer)
return encodeInteger((Integer)o);
else if(o instanceof ByteBuffer)
return encodeString((ByteBuffer)o);
else
throw new BencodingException("Error: Object not of valid type for Bencoding.");
}
/**
* Bencodes the specified byte string.
* @param string the byte string to bencode.
* @return a {@code byte[]} containing the bencoded form of the byte string.
*/
private static final byte[] encodeString(ByteBuffer string)
{
int length = string.array().length;
int num_digits = 1;
while((length /= 10) > 0)
{
num_digits++;
}
byte[] bencoded_string = new byte[length+num_digits+1];
bencoded_string[num_digits] = (byte)':';
System.arraycopy(string.array(), 0, bencoded_string, num_digits+1, length);
for(int i = num_digits-1; i >= 0; i--)
{
bencoded_string[i] = (byte)((length % 10)+48);
length /= 10;
}
return bencoded_string;
}
/**
* Bencodes the specified Integer.
* @param integer the Integer to bencode.
* @return a {@code byte[]} containing the bencoded form of the Integer.
*/
private static final byte[] encodeInteger(Integer integer)
{
int num_digits = 1;
int int_val = integer.intValue();
while((int_val /= 10) > 0)
++num_digits;
int_val = integer.intValue();
byte[] bencoded_integer = new byte[num_digits+2];
bencoded_integer[0] = (byte)'i';
bencoded_integer[bencoded_integer.length - 1] = (byte)'e';
for(int i = num_digits; i > 0; i--)
{
bencoded_integer[i] = (byte)((int_val % 10)+48);
int_val /= 10;
}
return bencoded_integer;
}
/**
* Bencodes the specified {@code ArrayList}.
* @param list the {@code ArrayList} to bencode.
* @return a {@code byte[]} containing the bencoded form of the {@code ArrayList}.
* @throws BencodingException if any of the objects in the list is not bencodable.
*/
@SuppressWarnings("unchecked")
private static final byte[] encodeList(ArrayList list) throws BencodingException
{
byte[][] list_segments = new byte[list.size()][];
for(int i = 0; i < list_segments.length;i++)
{
list_segments[i] = encode(list.get(i));
}
int total_length = 2;
for(int i = 0 ; i < list_segments.length; i++)
total_length += list_segments[i].length;
byte[] bencoded_list = new byte[total_length];
bencoded_list[0] = 'l';
bencoded_list[bencoded_list.length-1] = 'e';
int offset = 1;
for(int i = 0; i < list_segments.length; i++)
{
System.arraycopy(list_segments[i],0,bencoded_list,offset,list_segments[i].length);
offset += list_segments[i].length;
}
return bencoded_list;
}
/**
* Bencodes the specified {@code HashMap}.
* @param dictionary the {@code HashMap} to bencode.
* @return a {@code byte[]} containing the bnecoded form of the {@code HashMap}.
* @throws BencodingException if any of the objecdts in the map is not bencodable.
*/
private static final byte[] encodeDictionary(HashMap<ByteBuffer, Object> dictionary) throws BencodingException
{
TreeMap<ByteBuffer, Object> sorted_dictionary = new TreeMap<ByteBuffer, Object>();
sorted_dictionary.putAll(dictionary);
byte[][] dictionary_parts = new byte[sorted_dictionary.keySet().size()*2][];
int k = 0;
for(Iterator<ByteBuffer> i = sorted_dictionary.keySet().iterator(); i.hasNext();)
{
ByteBuffer key = i.next();
dictionary_parts[k++] = encodeString(key);
dictionary_parts[k++] = encode(sorted_dictionary.get(key));
}
int total_length = 2;
for(int i = 0; i < dictionary_parts.length; i++)
{
total_length += dictionary_parts[i].length;
}
byte[] bencoded_dictionary = new byte[total_length];
bencoded_dictionary[0] = 'd';
bencoded_dictionary[bencoded_dictionary.length-1] = 'e';
int offset = 1;
for(int i = 0; i < dictionary_parts.length; i++)
{
System.arraycopy(dictionary_parts[i],0,bencoded_dictionary,offset,dictionary_parts[i].length);
offset += dictionary_parts[i].length;
}
return bencoded_dictionary;
}
}