diff --git a/app/build.gradle b/app/build.gradle index 53da148f..94356ad7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,6 @@ dependencies { implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' implementation 'com.getbase:floatingactionbutton:1.10.1' implementation 'com.nispok:snackbar:2.11.0' - implementation 'com.github.aegnor:rencode-java:cb628e824e' implementation 'org.apache.openjpa:openjpa-lib:3.1.1' implementation 'net.iharder:base64:2.3.9' implementation('com.github.afollestad.material-dialogs:core:0.9.6.0@aar') { diff --git a/app/src/main/java/se/dimovski/rencode/Rencode.java b/app/src/main/java/se/dimovski/rencode/Rencode.java new file mode 100644 index 00000000..4adea6f7 --- /dev/null +++ b/app/src/main/java/se/dimovski/rencode/Rencode.java @@ -0,0 +1,32 @@ +package se.dimovski.rencode; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class Rencode +{ + + public static Object decode(byte[] data) throws IOException + { + final InputStream is = new ByteArrayInputStream(data); + final RencodeInputStream inputStream = new RencodeInputStream(is); + + final Object decoded = inputStream.readObject(); + inputStream.close(); + + return decoded; + } + + public static byte[] encode(Object obj) throws IOException + { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final RencodeOutputStream output = new RencodeOutputStream(baos); + output.writeObject(obj); + final byte[] encoded = baos.toByteArray(); + output.close(); + return encoded; + } + +} diff --git a/app/src/main/java/se/dimovski/rencode/RencodeInputStream.java b/app/src/main/java/se/dimovski/rencode/RencodeInputStream.java new file mode 100644 index 00000000..24d8f92e --- /dev/null +++ b/app/src/main/java/se/dimovski/rencode/RencodeInputStream.java @@ -0,0 +1,494 @@ +package se.dimovski.rencode; + +import java.io.DataInput; +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class RencodeInputStream extends FilterInputStream implements DataInput +{ + /** + * The charset that is being used for {@link String}s. + */ + private final String charset; + + /** + * Whether or not all byte-Arrays should be decoded as {@link String}s. + */ + private final boolean decodeAsString; + + /** + * Creates a {@link RencodeInputStream} with the default encoding. + */ + public RencodeInputStream(InputStream in) + { + this(in, Utils.UTF_8, false); + } + + /** + * Creates a {@link RencodeInputStream} with the given encoding. + */ + public RencodeInputStream(InputStream in, String charset) + { + this(in, charset, false); + } + + /** + * Creates a {@link RencodeInputStream} with the default encoding. + */ + public RencodeInputStream(InputStream in, boolean decodeAsString) + { + this(in, Utils.UTF_8, decodeAsString); + } + + /** + * Creates a {@link RencodeInputStream} with the given encoding. + */ + public RencodeInputStream(InputStream in, String charset, boolean decodeAsString) + { + super(in); + + if (charset == null) + { + throw new IllegalArgumentException("charset is null"); + } + + this.charset = charset; + this.decodeAsString = decodeAsString; + } + + /** + * Returns the charset that is used to decode {@link String}s. The default + * value is UTF-8. + */ + public String getCharset() + { + return charset; + } + + /** + * Returns true if all byte-Arrays are being turned into {@link String}s. + */ + public boolean isDecodeAsString() + { + return decodeAsString; + } + + /** + * Reads and returns an {@link Object}. + */ + public Object readObject() throws IOException + { + int token = readToken(); + + return readObject(token); + } + + /** + * Reads and returns an {@link Object}. + */ + protected Object readObject(int token) throws IOException + { + if (token == TypeCode.DICTIONARY) + { + return readMap0(Object.class); + } + else if (Utils.isFixedDictionary(token)) + { + return readMap0(Object.class, token); + } + else if (token == TypeCode.LIST) + { + return readList0(Object.class); + } + else if (Utils.isFixedList(token)) + { + return readList0(Object.class, token); + } + else if (Utils.isNumber(token)) + { + return readNumber0(token); + } + else if (token == TypeCode.FALSE || token == TypeCode.TRUE) + { + return readBoolean0(token); + } + else if (token == TypeCode.NULL) + { + return null; + } + else if (Utils.isDigit(token) || Utils.isFixedString(token)) + { + return readString(token, charset); + } + + throw new IOException("Not implemented: " + token); + } + + /** + * Reads and returns a {@link Map}. + */ + public Map readMap() throws IOException + { + return readMap(Object.class); + } + + /** + * Reads and returns a {@link Map}. + */ + public Map readMap(Class clazz) throws IOException + { + int token = readToken(); + + if (token != TypeCode.DICTIONARY) + { + throw new IOException(); + } + + return readMap0(clazz); + } + + private Map readMap0(Class clazz) throws IOException + { + Map map = new TreeMap(); + int token = -1; + while ((token = readToken()) != TypeCode.END) + { + readMapItem(clazz, token, map); + } + + return map; + } + + private Map readMap0(Class clazz, int token) throws IOException + { + Map map = new TreeMap(); + + int count = token - TypeCode.EMBEDDED.DICT_START; + for (int i = 0; i < count; i++) + { + readMapItem(clazz, readToken(), map); + } + + return map; + } + + private void readMapItem(Class clazz, int token, Map map) throws UnsupportedEncodingException, + IOException + { + String key = readString(token, charset); + T value = clazz.cast(readObject()); + + map.put(key, value); + } + + public int readToken() throws IOException + { + int token = super.read(); + if (token == -1) + { + throw new EOFException(); + } + return token; + } + + /** + * Reads and returns a {@link List}. + */ + public List readList() throws IOException + { + return readList(Object.class); + } + + /** + * Reads and returns a {@link List}. + */ + public List readList(Class clazz) throws IOException + { + int token = readToken(); + + if (token != TypeCode.LIST) + { + throw new IOException(); + } + + return readList0(clazz); + } + + private List readList0(Class clazz) throws IOException + { + List list = new ArrayList(); + int token = -1; + while ((token = readToken()) != TypeCode.END) + { + list.add(clazz.cast(readObject(token))); + } + return list; + } + + private List readList0(Class clazz, int token) throws IOException + { + List list = new ArrayList(); + int length = token - TypeCode.EMBEDDED.LIST_START; + for (int i = 0; i < length; i++) + { + list.add(clazz.cast(readObject())); + } + return list; + } + + public boolean readBoolean() throws IOException + { + return readBoolean0(readToken()); + } + + public boolean readBoolean0(int token) throws IOException + { + if (token == TypeCode.FALSE) + { + return false; + } + else if (token == TypeCode.TRUE) + { + return true; + } + + throw new IOException(); + } + + public byte readByte() throws IOException + { + return (byte) readToken(); + } + + public char readChar() throws IOException + { + return (char) readToken(); + } + + public double readDouble() throws IOException + { + return readNumber().doubleValue(); + } + + public float readFloat() throws IOException + { + return readNumber().floatValue(); + } + + public void readFully(byte[] dst) throws IOException + { + readFully(dst, 0, dst.length); + } + + public void readFully(byte[] dst, int off, int len) throws IOException + { + int total = 0; + + while (total < len) + { + int r = read(dst, total, len - total); + if (r == -1) + { + throw new EOFException(); + } + + total += r; + } + } + + public int readInt() throws IOException + { + return readNumber().intValue(); + } + + public String readLine() throws IOException + { + return readString(); + } + + public long readLong() throws IOException + { + return readNumber().longValue(); + } + + public short readShort() throws IOException + { + return readNumber().shortValue(); + } + + public String readUTF() throws IOException + { + return readString(Utils.UTF_8); + } + + public int readUnsignedByte() throws IOException + { + return readByte() & 0xFF; + } + + public int readUnsignedShort() throws IOException + { + return readShort() & 0xFFFF; + } + + /** + * Reads and returns a {@link Number}. + */ + public Number readNumber() throws IOException + { + int token = readToken(); + + if (!Utils.isNumber(token)) + { + throw new IOException(); + } + + return readNumber0(token); + } + + private Number readNumber0(int token) throws IOException + { + switch (token) + { + case TypeCode.BYTE: + return (int) readToBuffer(1).get(); + case TypeCode.SHORT: + return (int) readToBuffer(2).getShort(); + case TypeCode.INT: + return readToBuffer(4).getInt(); + case TypeCode.LONG: + return readToBuffer(8).getLong(); + case TypeCode.FLOAT: + return readToBuffer(4).getFloat(); + case TypeCode.DOUBLE: + return readToBuffer(8).getDouble(); + + case TypeCode.NUMBER: + return readNumber0(); + } + if (Utils.isNegativeFixedNumber(token)) + { + return TypeCode.EMBEDDED.INT_NEG_START - 1 - token; + } + else if (Utils.isPositiveFixedNumber(token)) + { + return TypeCode.EMBEDDED.INT_POS_START + token; + } + + throw new IOException("Unknown number. TypeCode: " + token); + } + + private ByteBuffer readToBuffer(int count) throws IOException + { + return ByteBuffer.wrap(readBytesFixed(count)); + } + + private Number readNumber0() throws IOException + { + StringBuilder buffer = new StringBuilder(); + + boolean decimal = false; + + int token = -1; + while ((token = readToken()) != TypeCode.END) + { + if (token == '.') + { + decimal = true; + } + + buffer.append((char) token); + } + + try + { + if (decimal) + { + return new BigDecimal(buffer.toString()); + } + else + { + return new BigInteger(buffer.toString()); + } + } + catch (NumberFormatException err) + { + throw new IOException("NumberFormatException", err); + } + } + + public int skipBytes(int n) throws IOException + { + return (int) skip(n); + } + + /** + * Reads and returns a byte-Array. + */ + public byte[] readBytes() throws IOException + { + int token = readToken(); + + return readBytes(token); + } + + /** + * Reads and returns a {@link String}. + */ + public String readString() throws IOException + { + return readString(charset); + } + + private String readString(String encoding) throws IOException + { + return readString(readToken(), encoding); + } + + private String readString(int token, String charset) throws IOException + { + if (Utils.isFixedString(token)) + { + int length = token - TypeCode.EMBEDDED.STR_START; + return new String(readBytesFixed(length), charset); + } + return new String(readBytes(token), charset); + } + + private byte[] readBytes(int token) throws IOException + { + int length = readLength(token); + return readBytesFixed(length); + } + + private byte[] readBytesFixed(int count) throws IOException + { + byte[] data = new byte[count]; + readFully(data); + return data; + } + + private int readLength(int token) throws IOException + { + StringBuilder buffer = new StringBuilder(); + buffer.append((char) token); + + while ((token = readToken()) != TypeCode.LENGTH_DELIM) + { + + buffer.append((char) token); + } + + return Integer.parseInt(buffer.toString()); + } +} diff --git a/app/src/main/java/se/dimovski/rencode/RencodeOutputStream.java b/app/src/main/java/se/dimovski/rencode/RencodeOutputStream.java new file mode 100644 index 00000000..4ed0e011 --- /dev/null +++ b/app/src/main/java/se/dimovski/rencode/RencodeOutputStream.java @@ -0,0 +1,404 @@ +package se.dimovski.rencode; + +import java.io.DataOutput; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +public class RencodeOutputStream extends FilterOutputStream implements DataOutput +{ + + /** + * The {@link String} charset. + */ + private final String charset; + + /** + * Creates a {@link RencodeOutputStream} with the default charset. + */ + public RencodeOutputStream(OutputStream out) + { + this(out, Utils.UTF_8); + } + + /** + * Creates a {@link RencodeOutputStream} with the given encoding. + */ + public RencodeOutputStream(OutputStream out, String charset) + { + super(out); + + if (charset == null) + { + throw new NullPointerException("charset"); + } + + this.charset = charset; + } + + /** + * Returns the charset that is used to encode {@link String}s. The default + * value is UTF-8. + */ + public String getCharset() + { + return charset; + } + + /** + * Writes an {@link Object}. + */ + public void writeObject(Object value) throws IOException + { + if (value == null) + { + writeNull(); + } + else if (value instanceof byte[]) + { + writeBytes((byte[]) value); + } + else if (value instanceof Boolean) + { + writeBoolean((Boolean) value); + + } + else if (value instanceof Character) + { + writeChar((Character) value); + + } + else if (value instanceof Number) + { + writeNumber((Number) value); + + } + else if (value instanceof String) + { + writeString((String) value); + + } + else if (value instanceof Collection) + { + writeCollection((Collection) value); + + } + else if (value instanceof Map) + { + writeMap((Map) value); + + } + else if (value instanceof Enum) + { + writeEnum((Enum) value); + + } + else if (value.getClass().isArray()) + { + writeArray(value); + + } + else + { + writeCustom(value); + } + } + + /** + * Writes a null value + */ + public void writeNull() throws IOException + { + write(TypeCode.NULL); + } + + /** + * Overwrite this method to write custom objects. The default implementation + * throws an {@link IOException}. + */ + protected void writeCustom(Object value) throws IOException + { + throw new IOException("Cannot encode " + value); + } + + /** + * Writes the given byte-Array + */ + public void writeBytes(byte[] value) throws IOException + { + writeBytes(value, 0, value.length); + } + + /** + * Writes the given byte-Array + */ + public void writeBytes(byte[] value, int offset, int length) throws IOException + { + write(value, offset, length); + } + + /** + * Writes a boolean + */ + public void writeBoolean(boolean value) throws IOException + { + write(value ? TypeCode.TRUE : TypeCode.FALSE); + } + + /** + * Writes a char + */ + public void writeChar(int value) throws IOException + { + writeByte(value); + } + + /** + * Writes a byte + */ + public void writeByte(int value) throws IOException + { + write(TypeCode.BYTE); + write(value); + } + + /** + * Writes a short + */ + public void writeShort(int value) throws IOException + { + write(TypeCode.SHORT); + ByteBuffer buffer = ByteBuffer.allocate(Utils.SHORT_BYTES).putShort((short) value); + write(buffer.array()); + } + + /** + * Writes an int + */ + public void writeInt(int value) throws IOException + { + write(TypeCode.INT); + ByteBuffer buffer = ByteBuffer.allocate(Utils.INTEGER_BYTES).putInt(value); + write(buffer.array()); + } + + /** + * Writes a long + */ + public void writeLong(long value) throws IOException + { + write(TypeCode.LONG); + ByteBuffer buffer = ByteBuffer.allocate(Utils.LONG_BYTES).putLong(value); + write(buffer.array()); + } + + /** + * Writes a float + */ + public void writeFloat(float value) throws IOException + { + write(TypeCode.FLOAT); + ByteBuffer buffer = ByteBuffer.allocate(Utils.FLOAT_BYTES).putFloat(value); + write(buffer.array()); + } + + /** + * Writes a double + */ + public void writeDouble(double value) throws IOException + { + write(TypeCode.DOUBLE); + ByteBuffer buffer = ByteBuffer.allocate(Utils.DOUBLE_BYTES).putDouble(value); + write(buffer.array()); + } + + /** + * Writes a {@link Number} + */ + public void writeNumber(Number num) throws IOException + { + if (num instanceof Float) + { + writeFloat(num.floatValue()); + } + else if (num instanceof Double) + { + writeDouble(num.doubleValue()); + } + if (0 <= num.intValue() && num.intValue() < TypeCode.EMBEDDED.INT_POS_COUNT) + { + write(TypeCode.EMBEDDED.INT_POS_START + num.intValue()); + } + else if (-TypeCode.EMBEDDED.INT_NEG_COUNT <= num.intValue() && num.intValue() < 0) + { + write(TypeCode.EMBEDDED.INT_NEG_START - 1 - num.intValue()); + } + else if (Byte.MIN_VALUE <= num.intValue() && num.intValue() < Byte.MAX_VALUE) + { + writeByte(num.byteValue()); + } + else if (Short.MIN_VALUE <= num.intValue() && num.intValue() < Short.MAX_VALUE) + { + writeShort(num.shortValue()); + } + else if (Integer.MIN_VALUE <= num.longValue() && num.longValue() < Integer.MAX_VALUE) + { + writeInt(num.intValue()); + } + else if (Long.MIN_VALUE <= num.longValue() && num.longValue() < Long.MAX_VALUE) + { + writeLong(num.longValue()); + } + else + { + String number = num.toString(); + write(TypeCode.NUMBER); + write(number.getBytes(charset)); + write(TypeCode.END); + } + } + + /** + * Writes a {@link String} + */ + public void writeString(String value) throws IOException + { + int len = value.length(); + if (len < TypeCode.EMBEDDED.STR_COUNT) + { + write(TypeCode.EMBEDDED.STR_START + len); + } + else + { + String lenString = Integer.toString(len); + writeBytes(lenString.getBytes(charset)); + write(TypeCode.LENGTH_DELIM); + } + + writeBytes(value.getBytes(charset)); + } + + /** + * Writes a {@link Collection}. + */ + public void writeCollection(Collection value) throws IOException + { + boolean useEndToken = value.size() >= TypeCode.EMBEDDED.LIST_COUNT; + if (useEndToken) + { + write(TypeCode.LIST); + } + else + { + write(TypeCode.EMBEDDED.LIST_START + value.size()); + } + + for (Object element : value) + { + writeObject(element); + } + + if (useEndToken) + { + write(TypeCode.END); + } + } + + /** + * Writes a {@link Map}. + */ + public void writeMap(Map map) throws IOException + { + if (!(map instanceof SortedMap)) + { + map = new TreeMap(map); + } + + boolean untilEnd = map.size() >= TypeCode.EMBEDDED.DICT_COUNT; + + if (untilEnd) + { + write(TypeCode.DICTIONARY); + } + else + { + write(TypeCode.EMBEDDED.DICT_START + map.size()); + } + + for (Map.Entry entry : map.entrySet()) + { + writeObject(entry.getKey()); + writeObject(entry.getValue()); + } + + if (untilEnd) + { + write(TypeCode.END); + } + } + + /** + * Writes an {@link Enum}. + */ + public void writeEnum(Enum value) throws IOException + { + writeString(value.name()); + } + + /** + * Writes an array + */ + public void writeArray(Object value) throws IOException + { + int length = Array.getLength(value); + boolean useEndToken = length >= TypeCode.EMBEDDED.LIST_COUNT; + if (useEndToken) + { + write(TypeCode.LIST); + } + else + { + write(TypeCode.EMBEDDED.LIST_START + length); + } + + for (int i = 0; i < length; i++) + { + writeObject(Array.get(value, i)); + } + + if (useEndToken) + { + write(TypeCode.END); + } + } + + /** + * Writes the given {@link String} + */ + public void writeBytes(String value) throws IOException + { + writeString(value); + } + + /** + * Writes the given {@link String} + */ + public void writeChars(String value) throws IOException + { + writeString(value); + } + + /** + * Writes an UTF encoded {@link String} + */ + public void writeUTF(String value) throws IOException + { + writeBytes(value.getBytes(Utils.UTF_8)); + } +} diff --git a/app/src/main/java/se/dimovski/rencode/TypeCode.java b/app/src/main/java/se/dimovski/rencode/TypeCode.java new file mode 100644 index 00000000..10c7de07 --- /dev/null +++ b/app/src/main/java/se/dimovski/rencode/TypeCode.java @@ -0,0 +1,47 @@ +package se.dimovski.rencode; + +public class TypeCode +{ + // The bencode 'typecodes' such as i, d, etc have been + // extended and relocated on the base-256 character set. + public static final char LIST = 59; + public static final char DICTIONARY = 60; + public static final char NUMBER = 61; + public static final char BYTE = 62; + public static final char SHORT = 63; + public static final char INT = 64; + public static final char LONG = 65; + public static final char FLOAT = 66; + public static final char DOUBLE = 44; + public static final char TRUE = 67; + public static final char FALSE = 68; + public static final char NULL = 69; + public static final char END = 127; + public static final char LENGTH_DELIM = ':'; + + /* + * TypeCodes with embedded values/lengths + */ + public static class EMBEDDED + { + // Positive integers + public static final int INT_POS_START = 0; + public static final int INT_POS_COUNT = 44; + + // Negative integers + public static final int INT_NEG_START = 70; + public static final int INT_NEG_COUNT = 32; + + // Dictionaries + public static final int DICT_START = 102; + public static final int DICT_COUNT = 25; + + // Strings + public static final int STR_START = 128; + public static final int STR_COUNT = 64; + + // Lists + public static final int LIST_START = STR_START + STR_COUNT; + public static final int LIST_COUNT = 64; + } +} diff --git a/app/src/main/java/se/dimovski/rencode/Utils.java b/app/src/main/java/se/dimovski/rencode/Utils.java new file mode 100644 index 00000000..d4007652 --- /dev/null +++ b/app/src/main/java/se/dimovski/rencode/Utils.java @@ -0,0 +1,74 @@ +package se.dimovski.rencode; + +public class Utils +{ + // Character Encodings + public final static String UTF_8 = "UTF-8"; + public final static String ISO_8859 = "ISO-8859-1"; + + // Byte-lengths for types + public static final int SHORT_BYTES = Short.SIZE / Byte.SIZE; + public static final int INTEGER_BYTES = Integer.SIZE / Byte.SIZE; + public static final int LONG_BYTES = Long.SIZE / Byte.SIZE; + public static final int FLOAT_BYTES = Float.SIZE / Byte.SIZE; + public static final int DOUBLE_BYTES = Double.SIZE / Byte.SIZE; + + // Maximum length of integer when written as base 10 string. + public static final int MAX_INT_LENGTH = 64; + + private static boolean tokenInRange(int token, int start, int count) + { + return start <= token && token < (start + count); + } + + public static boolean isNumber(int token) + { + switch (token) + { + case TypeCode.NUMBER: + case TypeCode.BYTE: + case TypeCode.SHORT: + case TypeCode.INT: + case TypeCode.LONG: + case TypeCode.FLOAT: + case TypeCode.DOUBLE: + return true; + } + return isFixedNumber(token); + } + + public static boolean isFixedNumber(int token) + { + return isPositiveFixedNumber(token) || isNegativeFixedNumber(token); + } + + public static boolean isPositiveFixedNumber(int token) + { + return tokenInRange(token, TypeCode.EMBEDDED.INT_POS_START, TypeCode.EMBEDDED.INT_POS_COUNT); + } + + public static boolean isNegativeFixedNumber(int token) + { + return tokenInRange(token, TypeCode.EMBEDDED.INT_NEG_START, TypeCode.EMBEDDED.INT_NEG_COUNT); + } + + public static boolean isFixedList(int token) + { + return tokenInRange(token, TypeCode.EMBEDDED.LIST_START, TypeCode.EMBEDDED.LIST_COUNT); + } + + public static boolean isFixedDictionary(int token) + { + return tokenInRange(token, TypeCode.EMBEDDED.DICT_START, TypeCode.EMBEDDED.DICT_COUNT); + } + + public static boolean isFixedString(int token) + { + return tokenInRange(token, TypeCode.EMBEDDED.STR_START, TypeCode.EMBEDDED.STR_COUNT); + } + + public static boolean isDigit(int token) + { + return '0' <= token && token <= '9'; + } +}