/*
* 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.*;
import java.util.*;
/**
* The Bencoder class contains operations needed to
* bencode and bdecode objects used in the BitTorrent protocol.
*
* The BitTorrent protocol specifies 4 object types: signed integers,
* lists, dictionaries and byte strings
* bencoded data types which are mapped onto {@link Number}, {@link List},
* {@link Map}, and {@link String} encoded in UTF-8, respectively.
*
* @author Chris Lauderdale
* @version 1.0
*/
final class Bencoder
{
private static final class IRef
{
public int i;
public ByteArrayOutputStream infoBytes = null;
public IRef(int x)
{
super();
i = x;
}
public void append(byte[] b, int st, int len)
{
if (infoBytes != null)
infoBytes.write(b, st, len);
}
public void append(char b)
{
if (infoBytes != null)
infoBytes.write(b);
}
public ByteArrayOutputStream createBAOS()
{
return infoBytes = new ByteArrayOutputStream();
}
}
static final Class INTEGER_CLASS = Long.class;
static final Class BYTE_STRING_CLASS = byte[].class;
static final Class LIST_CLASS = List.class;
static final Class DICTIONARY_CLASS = Map.class;
static final Object INFO_BYTES = new Object();
/**
* Bdecodes an object from an InputStream. The first byte
* returned by is must be the beginning of the bencoded
* object.
*
* @param is
* the input stream from which to bdecode an object
* @return the object represented by the bencoded byte[]
*/
static Object bdecode(InputStream is) throws IOException
{
byte[] response = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int amt;
while ((amt = is.read(response)) >= 0)
{
if (amt == 0)
Thread.yield();
else
baos.write(response);
}
return bdecode(baos.toByteArray());
}
/**
* Bdecodes an object from a byte[]. bytes[0]
* must be the beginning of the bencoded object.
*
* @param bytes
* the byte[] from which to bdecode an object
* @return the object represented by bytes
* @throws IndexOutOfBoundsException
*/
static Object bdecode(byte[] bytes)
{
if (bytes.length < 1)
return null;
return bdecode(bytes, 0);
}
/**
* Bdecodes an bencoded object contained in bytes specifying
* the byte[] from which to bdecode an object and the offset
* within bytes at which the object begins.
* start such that 0 ≤ start < bytes.length.
*
* @param bytes
* the byte[] from which to bdeocde an object
* @param start
* the index within bytes that marks the beginning
* of the object to be bdecoded
* @return the object bencoded beginning at bytes[start]
* @throws IndexOutOfBoundsException
*/
static Object bdecode(byte[] bytes, int start)
{
return bdecode(bytes, start, new IRef(0));
}
/**
* Bdecodes a byte[] containing a bencoded object beginning
* at index start. TODO: Something about ind here.
*
* @param bytes
* the byte[] containing the bencoded object
* @param start
* the index in bytes that marks the beginning of
* the bencoded object.
* @param ind contains the bytes of the bencoded info dictionary
* that have been encountered so far
* @return the object bencoded beginning at byte[start]
*/
private static Object bdecode(byte[] bytes, int start, IRef ind)
{
if (bytes[start] == 'i')
return decodeInt(bytes, start, ind);
if (bytes[start] == 'l')
return decodeList(bytes, start, ind);
if (bytes[start] == 'd')
return decodeDict(bytes, start, ind);
if (bytes[start] < '0' || bytes[start] > '9')
throw new IllegalArgumentException("Invalid bencoded byte stream.");
return decodeBytes(bytes, start, ind);
}
/**
* Bdecodes a single signed integer. bytes[start] is the
* first byte of the bencoded integer.
*
* @param bytes the byte[] storing the bencoded integer
* @param start the index in bytes that marks the beginning
* of the bencoded integer
* @param ind contains the bytes of the bencoded info dictionary
* that have been encountered so far
* @return the Integer representation of the bencoded
* integer
*/
private static Integer decodeInt(byte[] bytes, int start, IRef ind)
{
for (int i = start + 1; i < bytes.length; i++)
if (bytes[i] == 'e')
{
final Integer rv = Integer.decode(new String(bytes, (byte) 0,
start + 1, i - (start + 1)));
ind.i = i + 1;
ind.append(bytes, start, ind.i - start);
return rv;
}
throw new IllegalArgumentException(
"Invalid bencoded byte stream: Bad integer");
}
/**
* Bdecodes a bencoded list object beginning at bytes[start].
*
* @param bytes the byte[] storing the bencoded list
* @param start the index in bytes that marks the beginning
* of the list
* @param ind contains the bytes of the bencoded info dictionary
* that have been encountered so far
* @return the List representation of the bencoded list
*/
private static List decodeList(byte[] bytes, int start, IRef ind)
{
final List rv = new LinkedList();
ind.append('l');
++start;
while (bytes[start] != 'e')
{
rv.add(bdecode(bytes, start, ind));
start = ind.i;
}
ind.append('e');
++ind.i;
return rv;
}
/**
* Bdecodes a bencoded dictionary object beginning at
* bytes[start].
*
* @param bytes the byte[] storing the bencoded dictionary
* @param start the index in bytes that marks the beginning
* of the dictionary
* @param ind contains the bytes of the bencoded info dictionary
* that have been encountered so far
* @return the Map representation of the dictionary beginning
* at bytes[start]
*/
private static Map decodeDict(byte[] bytes, int start, IRef ind)
{
final Map rv = new HashMap();
boolean haveInfoBytes = false;
ind.append('d');
++start;
while (bytes[start] != 'e')
{
byte[] f = decodeBytes(bytes, start, ind);
start = ind.i;
final String strf = new String(f, 0);
if (strf.equalsIgnoreCase("info") && ind.infoBytes == null)
{
ind.createBAOS();
haveInfoBytes = true;
}
rv.put(strf, bdecode(bytes, start, ind));
start = ind.i;
}
++ind.i;
if (haveInfoBytes)
{
rv.put(INFO_BYTES, ind.infoBytes.toByteArray());
ind.infoBytes = null;
}
ind.append('e');
return rv;
}
/**
* Bdecodes a bencoded byte string beginning at bytes[start].
*
* @param bytes the byte[] storing the bencoded byte string
* @param start the index in bytes that marks the beginning of
* the dictionary
* @param ind contains the bytes of the bencoded info dictionary
* that have been encountered so far
* @return the byte[] representation of the byte string
* beginning at bytes[start]
*/
private static byte[] decodeBytes(byte[] bytes, int start, IRef ind)
{
for (int i = start; i < bytes.length; i++)
if (bytes[i] == ':')
{
final byte[] rv = new byte[Integer.parseInt(new String(bytes,
(byte) 0, start, i - start))];
System.arraycopy(bytes, ++i, rv, 0, rv.length);
ind.i = i + rv.length;
ind.append(bytes, start, ind.i - start);
return rv;
}
throw new IllegalArgumentException(
"Invalid bencoded byte stream: Invalid byte string");
}
/**
* Bencodes an object into a byte[]. If o is
* not a type specified in the BitTorrent protocol, then its string
* representation is bencoded as a byte string.
*
* @param o
* the object to be bencoded
* @return the bencoded byte[] representing o
*/
static byte[] bencode(Object o)
{
final ByteArrayOutputStream rv = new ByteArrayOutputStream();
try
{
bencode(o, rv);
}
catch (IOException _)
{}
return rv.toByteArray();
}
/**
* Writes the bencoded form of o into buf. If
* o is not a type specified in the BitTorrent protocol, then
* its string representation is bencoded as a byte string.
*
* @param o the object to be bencoded
* @param buf the OutputStream into which the bencoded form of
* o is written
*/
private static void bencode(Object o, OutputStream buf) throws IOException
{
if (o instanceof Iterable)
encodeIterated(((Iterable) o).iterator(), buf);
else if (o instanceof Iterator)
encodeIterated((Iterator) o, buf);
else if (o instanceof Map)
encodeMap((Map) o, buf);
else if (o instanceof Number)
encodeNumber((Number) o, buf);
else if (o instanceof byte[])
encodeBytes((byte[]) o, buf);
else
try
{
encodeBytes(o.toString().getBytes("UTF-8"), buf);
}
catch (UnsupportedEncodingException _)
{
encodeBytes(o.toString().getBytes(), buf);
}
}
/**
* Writes the bencoded form of the Iterator i
* into buf.
*
* @param o the Iterator object to be bencoded
* @param buf the OutputStream into which the bencoded form of
* o is written
*/
private static void encodeIterated(Iterator i, OutputStream buf)
throws IOException
{
buf.write('l');
while (i.hasNext())
{
bencode(i.next(), buf);
}
buf.write('e');
}
/**
* Writes the bencoded form of the Map m into
* buf
*
* @param m the Map object to be bencoded
* @param buf the OutputStream into which the bencoded form of
* m is written
*/
private static void encodeMap(Map m, OutputStream buf) throws IOException
{
buf.write('d');
for (Iterator i = m.keySet().iterator(); i.hasNext();)
{
final Object o = i.next();
final Object p = m.get(o);
if (p == null)
continue;
byte[] bytes;
if (o instanceof byte[])
bytes = (byte[]) o;
else
try
{
bytes = o.toString().getBytes("UTF-8");
}
catch (UnsupportedEncodingException _)
{
bytes = o.toString().getBytes();
}
encodeBytes(bytes, buf);
bencode(p, buf);
}
buf.write('e');
}
/**
* Writes the bencoded form of the Number n into
* buf
*
* @param n the Number object to be bencoded
* @param buf the OutputStream into which the bencoded form of
* n is written
*/
private static void encodeNumber(Number n, OutputStream buf)
throws IOException
{
buf.write('i');
buf.write(Long.toString(n.longValue()).getBytes());
buf.write('e');
}
/**
* Writes the bencoded form of the byte[] b into
* buf
*
* @param b the byte[] object to be hashed
* @param buf the OutputStream into which the bencoded form of
* n is written
*/
private static void encodeBytes(byte[] b, OutputStream buf)
throws IOException
{
buf.write(Integer.toString(b.length).getBytes());
buf.write(':');
buf.write(b);
}
}