/* * 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); } }