package org.limewire.util; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * Convert Java objects into bencoded data. * <p> * Call BEncoder.encode(OutputStream, Object) to bencode a given Object and write the bencoded data to the given OutputStream. * <p> * Bencoded data is composed of strings, numbers, lists, and dictionaries. * Strings are prefixed by their length, like "5:hello". * Numbers are written as text numerals between the letters "i" and "e", like "i87e". * You can list any number of bencoded pieces of data between "l" for list and "e" for end. * A dictionary is a list of key and value pairs between "d" and "e". * The keys have to be strings, and they have to be in alphabetical order. * <p> * BitTorrent uses a simple and extensible data format called bencoding. * More information on bencoding is on the Web at: * http://en.wikipedia.org/wiki/Bencoding * http://www.bittorrent.org/protocol.html in the section titled "The connectivity is as follows". */ public class BEncoder { /** Identifies a bencoded real number. */ public static final byte I; /** Identifies a bencoded rational number. */ public static final byte R; /** Identifies a bencoded dictionary. */ public static final byte D; /** Identifies a bencoded list. */ public static final byte L; /** Marks the end of something in bencoding. */ public static final byte E; /** Markers for true and false tokens */ public static final byte TRUE; public static final byte FALSE; /** Separates the length from the string in the data of a bencoded string. */ public final static byte COLON; private static final String ASCII = "ISO-8859-1"; static { byte i = 0; byte d = 0; byte l = 0; byte e = 0; byte colon = 0; byte t = 0; byte f = 0; byte r = 0; try { i = "i".getBytes(ASCII)[0]; d = "d".getBytes(ASCII)[0]; l = "l".getBytes(ASCII)[0]; e = "e".getBytes(ASCII)[0]; colon = ":".getBytes(ASCII)[0]; t = "t".getBytes(ASCII)[0]; f = "f".getBytes(ASCII)[0]; r = "r".getBytes(ASCII)[0]; } catch (UnsupportedEncodingException impossible) { // TODO: connect to the error service } COLON = colon; I = i; D = d; L = l; E = e; TRUE = t; FALSE = f; R = r; } private final boolean fail, bool; private final String encoding; private final OutputStream output; private BEncoder(OutputStream output, boolean fail, boolean bool, String encoding) { this.fail = fail; this.encoding = encoding; this.output = output; this.bool = bool; } public static BEncoder getEncoder(OutputStream out) { return new BEncoder(out, false, true, ASCII); } public static BEncoder getEncoder(OutputStream out, boolean fail, boolean bool, String encoding) { return new BEncoder(out, fail, bool, encoding); } /** * Bencodes the given byte array to the given OutputStream. * <p> * Writes the length, a colon, and then the text. * For example, the byte array ['h', 'e', 'l', 'l', 'o'] becomes the bencoded bytes "5:hello". * * @param b the byte array to bencode and write */ public void encodeByteArray(byte[] b) throws IOException { String length = String.valueOf(b.length); output.write(length.getBytes(ASCII)); output.write(COLON); output.write(b); } /** * Bencodes the given Number to the given OutputStream. * <p> * Writes the base 10 digits of the number between the letters "i" and "e". * For example, the number 87 becomes the bencoded ASCII bytes "i87e". * * @param n the number to bencode and write */ public void encodeInt(Number n) throws IOException { String numerals = String.valueOf(n.longValue()); output.write(I); output.write(numerals.getBytes(ASCII)); output.write(E); } /** * Bencodes the given Rational Number to the given OutputStream. * <p> * Writes the base 10 digits of the number's internal memory representation * between the letters "r" and "e". * * @param n the number to bencode and write */ public void encodeRational(Number n) throws IOException { String numerals = String.valueOf(Double.doubleToLongBits(n.doubleValue())); output.write(R); output.write(numerals.getBytes(ASCII)); output.write(E); } /** * Bencodes the given Iterable to the given OutputStream. * <p> * Writes "l" for list, the bencoded-form of each of the given objects, and then "e" for end. * * @param iterable a Java Iterable object to bencode and write */ public void encodeList(Iterable<?> iterable) throws IOException { output.write(L); for(Object next : iterable) encode(next); output.write(E); } /** * Bencodes the given Map to the given OutputStream. Any String objects * are encoded using ASCII. * <p> * Writes a bencoded dictionary, which is a list of keys and values which looks like this: * <pre> * d * 5:color 5:green * 6:flavor 4:lime * 5:shape 5:round * e * </pre> * The bencoded data starts "d" for dictionary and ends "e" for end. * In the middle are pairs of bencoded values. * The keys have to be strings, while the values can be strings, numbers, lists, or more dictionaries. * The keys have to be in alphabetical order. * * @param map the Java Map object to bencode and write */ public void encodeDict(Map<?, ?> map) throws IOException { // The BitTorrent specification requires that dictionary keys are sorted in alphanumeric order SortedMap<String, Object> sorted = new TreeMap<String, Object>(); for(Map.Entry<?, ?> entry : map.entrySet()) sorted.put(entry.getKey().toString(), entry.getValue()); output.write(D); for(Map.Entry<String, Object> entry : sorted.entrySet()) { if (isValidType(entry.getKey()) && isValidType(entry.getValue())) { encodeByteArray(entry.getKey().getBytes(encoding)); encode(entry.getValue()); } else if (fail) throw new IllegalArgumentException(); } output.write(E); } /** * Describes a given object using bencoding, and writes the bencoded data to the given stream. * <p> * To write a bencoded dictionary, pass a Map object. * To write a bencoded list, pass a List object. * To write a bencoded number, pass a Number object. * To write a bencoded string, pass a String or just a byte array. * * @param object the Java Object to bencode and write. * @throws IOException if there was a problem reading from the OutputStream. * IllegalArgumentException If you pass an object that isn't a Map, List, Number, String, or byte array. */ private void encode(Object object) throws IOException { if (object instanceof Map) encodeDict((Map)object); else if (object instanceof Iterable<?>) encodeList((Iterable<?>)object); else if (object instanceof Number) { if (object instanceof Double || object instanceof Float) encodeRational((Number)object); else encodeInt((Number)object); } else if (object instanceof String) encodeByteArray(((String)object).getBytes(encoding)); else if (object instanceof byte[]) encodeByteArray((byte[])object); else if (object instanceof Boolean) encodeBoolean((Boolean) object); else if (fail) throw new IllegalArgumentException(); } public void encodeBoolean(boolean value) throws IOException { if (bool) output.write(value ? TRUE : FALSE); else encodeInt(value ? 1 : 0); } private static boolean isValidType(Object o ) { return (o instanceof Map) || (o instanceof List )|| (o instanceof Number) || (o instanceof String) || (o instanceof byte[]) || (o instanceof Boolean); } }