package org.torrent.internal.bencoding; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import org.torrent.internal.util.Validator; /** * Helper class to decode "bencoded" data. * * @author Dennis "Bytekeeper" Waldherr * */ public class BDecoder { private static class BListImpl extends ArrayList<Object> implements BList { /** * */ private static final long serialVersionUID = 1L; @Override public Integer getInteger(int index) throws BTypeException { return intOf(get(index)); } @Override public Long getLong(int index) throws BTypeException { return longOf(get(index)); } @Override public String getString(int index) throws BTypeException { return stringOf(get(index)); } @Override public BList getList(int index) throws BTypeException { Object tmp = get(index); if (tmp instanceof BList) { return (BList) tmp; } throw new BTypeException("Unexpected type: " + tmp.getClass()); } @Override public BMap getMap(int index) throws BTypeException { Object tmp = get(index); if (tmp instanceof BMap) { return (BMap) tmp; } throw new BTypeException("Unexpected type: " + tmp.getClass()); } } private static class BMapImpl extends HashMap<String, Object> implements BMap { /** * */ private static final long serialVersionUID = 1L; @Override public Integer getInteger(String key) throws BTypeException { return intOf(get(key)); } @Override public Long getLong(String key) throws BTypeException { return longOf(get(key)); } @Override public String getString(String key) throws BTypeException { return stringOf(get(key)); } @Override public BList getList(String key) throws BTypeException { Object tmp = get(key); if (tmp instanceof BList) { return (BList) tmp; } throw new BTypeException("Unexpected type: " + tmp.getClass()); } @Override public BMap getMap(String key) throws BTypeException { Object tmp = get(key); if (tmp instanceof BMap) { return (BMap) tmp; } throw new BTypeException("Unexpected type: " + tmp.getClass()); } } private static final int MAX_STRING_LENGTH = 1024 * 1024; /** * Converts a string result from bencoding to a java String. If the data is * already a String or null it is simply returned. * * @param data * @return * @throws BTypeException */ private static String stringOf(Object data) throws BTypeException { if (data == null) { return null; } try { if (data instanceof byte[]) { return new String((byte[]) data, "UTF-8"); } else if (data instanceof String) { return (String) data; } else { throw new BTypeException("Unsupported type: " + data.getClass()); } } catch (UnsupportedEncodingException e) { throw new Error(e); } } /** * Converts a constructed or decoded integer to a java Integer. If the data * is already an Integer or null it is simply returned. * * @param data * @return * @throws BTypeException */ private static Integer intOf(Object data) throws BTypeException { if (data == null) { return null; } if (data instanceof BigInteger) { return ((BigInteger) data).intValue(); } else if (data instanceof Integer) { return (Integer) data; } else { throw new BTypeException("Unsupported type: " + data.getClass()); } } /** * Converts a constructed or decoded integer to a java Integer. If the data * is already an Integer or null it is simply returned. * * @param data * @return * @throws BTypeException */ private static Long longOf(Object data) throws BTypeException { if (data == null) { return null; } if (data instanceof BigInteger) { return ((BigInteger) data).longValue(); } else if (data instanceof Integer) { return Long.valueOf((Integer) data); } else if (data instanceof Long) { return (Long) data; } else { throw new BTypeException("Unsupported type: " + data.getClass()); } } /** * Decodes bencoded data. * * @param bencoded * @return * @throws BDecodingException */ public static Object bdecode(byte[] bencoded) throws BDecodingException { Validator.notNull(bencoded, "Array is null!"); try { return bdecode(new ByteArrayInputStream(bencoded)); } catch (IOException e) { throw new BDecodingException(e); } } /** * Reads one bencoded element. * * @param in * @return * @throws IOException * @throws BDecodingException */ public static Object bdecode(InputStream in) throws IOException, BDecodingException { return bdecode(new PushbackInputStream(in)); } private static Object bdecode(PushbackInputStream in) throws IOException, BDecodingException { int head = in.read(); switch (head) { case -1: throw new EOFException(); case 'l': return bdecodeList(in); case 'i': return bdecodeInteger(in); case 'd': return bdecodeDictionary(in); default: if (Character.isDigit(head)) { in.unread(head); return bdecodeString(in); } } throw new BDecodingException("Parameter is not bencoded data."); } private static BMap bdecodeDictionary(PushbackInputStream in) throws IOException, BDecodingException { assert in != null; BMap map = new BMapImpl(); int head; while ((head = in.read()) != 'e') { if (head < 0) { throw new EOFException(); } in.unread(head); String key; try { key = stringOf(bdecodeString(in)); } catch (BTypeException e) { throw new BDecodingException(e); } map.put(key, bdecode(in)); } return map; } private static byte[] bdecodeString(PushbackInputStream in) throws IOException, BDecodingException { assert in != null; int len = 0; int head; while ((head = in.read()) != ':') { if (head < 0) { throw new EOFException(); } len = len * 10 + (head - '0'); if (len > MAX_STRING_LENGTH) { throw new BDecodingException("Encoded string length exceeds " + MAX_STRING_LENGTH + " bytes!"); } } byte data[] = new byte[len]; int off = 0; while (len > 0) { int read = in.read(data, off, len); len -= read; off += read; } return data; } private static Object bdecodeInteger(PushbackInputStream in) throws IOException, BDecodingException { assert in != null; StringBuilder b = new StringBuilder(); int head; while ((head = in.read()) != 'e') { if (head < 0) { throw new EOFException(); } if (!Character.isDigit(head)) { throw new BDecodingException("Expected digit but got: " + head); } b.append((char) head); } return new BigInteger(b.toString()); } private static BList bdecodeList(PushbackInputStream in) throws IOException, BDecodingException { assert in != null; BList list = new BListImpl(); int head; while ((head = in.read()) != 'e') { if (head < 0) { throw new EOFException(); } in.unread(head); list.add(bdecode(in)); } return list; } }