Eric Kok
3 years ago
committed by
GitHub
21 changed files with 1085 additions and 160 deletions
@ -0,0 +1,29 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,405 @@ |
|||||||
|
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<String, ?> readMap() throws IOException { |
||||||
|
return readMap(Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link Map}. |
||||||
|
*/ |
||||||
|
public <T> Map<String, T> readMap(Class<T> clazz) throws IOException { |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
if (token != TypeCode.DICTIONARY) { |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
return readMap0(clazz); |
||||||
|
} |
||||||
|
|
||||||
|
private <T> Map<String, T> readMap0(Class<T> clazz) throws IOException { |
||||||
|
Map<String, T> map = new TreeMap<String, T>(); |
||||||
|
int token = -1; |
||||||
|
while ((token = readToken()) != TypeCode.END) { |
||||||
|
readMapItem(clazz, token, map); |
||||||
|
} |
||||||
|
|
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> Map<String, T> readMap0(Class<T> clazz, int token) throws IOException { |
||||||
|
Map<String, T> map = new TreeMap<String, T>(); |
||||||
|
|
||||||
|
int count = token - TypeCode.EMBEDDED.DICT_START; |
||||||
|
for (int i = 0; i < count; i++) { |
||||||
|
readMapItem(clazz, readToken(), map); |
||||||
|
} |
||||||
|
|
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> void readMapItem(Class<T> clazz, int token, Map<String, T> 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 <T> List<T> readList(Class<T> clazz) throws IOException { |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
if (token != TypeCode.LIST) { |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
return readList0(clazz); |
||||||
|
} |
||||||
|
|
||||||
|
private <T> List<T> readList0(Class<T> clazz) throws IOException { |
||||||
|
List<T> list = new ArrayList<T>(); |
||||||
|
int token = -1; |
||||||
|
while ((token = readToken()) != TypeCode.END) { |
||||||
|
list.add(clazz.cast(readObject(token))); |
||||||
|
} |
||||||
|
return list; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> List<T> readList0(Class<T> clazz, int token) throws IOException { |
||||||
|
List<T> list = new ArrayList<T>(); |
||||||
|
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()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,321 @@ |
|||||||
|
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<Object, Object>(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)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
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'; |
||||||
|
} |
||||||
|
} |
Binary file not shown.
@ -1,5 +1,5 @@ |
|||||||
distributionBase=GRADLE_USER_HOME |
distributionBase=GRADLE_USER_HOME |
||||||
distributionPath=wrapper/dists |
distributionPath=wrapper/dists |
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip |
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip |
||||||
zipStoreBase=GRADLE_USER_HOME |
zipStoreBase=GRADLE_USER_HOME |
||||||
zipStorePath=wrapper/dists |
zipStorePath=wrapper/dists |
||||||
|
Loading…
Reference in new issue