/*
* BEncoder.java
*
* Created on June 4, 2003, 10:17 PM
* Copyright (C) 2003, 2004, 2005, 2006 Aelitis, All Rights Reserved.
*
* This 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 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 program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* AELITIS, SAS au capital de 46,603.30 euros
* 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
*/
package com.frostwire.torrent;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* A set of utility methods to encode a Map into a bencoded array of byte.
* integer are represented as Long, String as byte[], dictionnaries as Map, and list as List.
*
* @author TdC_VgA
*/
final class BEncoder {
private static final int BUFFER_DOUBLE_LIMIT = 256 * 1024;
private static final byte[] MINUS_1_BYTES = "-1".getBytes();
public static byte[] encode(Map<String, Object> object) throws IOException {
return (encode(object, false));
}
public static byte[] encode(Map<String, Object> object, boolean url_encode) throws IOException {
BEncoder encoder = new BEncoder(url_encode);
encoder.encodeObject(object);
return (encoder.toByteArray());
}
private byte[] current_buffer = new byte[256];
private int current_buffer_pos = 0;
private byte[][] old_buffers;
private byte[] int_buffer = new byte[12];
private boolean url_encode;
private BEncoder(boolean _url_encode) {
url_encode = _url_encode;
}
private boolean encodeObject(Object object)
throws IOException {
if (object instanceof BEncodableObject) {
object = ((BEncodableObject) object).toBencodeObject();
}
if (object instanceof String || object instanceof Float || object instanceof Double) {
String tempString = (object instanceof String) ? (String) object : String.valueOf(object);
// usually this is simpler to encode by hand as chars < 0x80 map directly in UTF-8
boolean simple = true;
int char_count = tempString.length();
byte[] encoded = new byte[char_count];
for (int i = 0; i < char_count; i++) {
char c = tempString.charAt(i);
if (c < 0x80) {
encoded[i] = (byte) c;
} else {
simple = false;
break;
}
}
if (simple) {
writeInt(char_count);
writeChar(':');
writeBytes(encoded);
} else {
ByteBuffer bb = Constants.DEFAULT_CHARSET.encode(tempString);
writeInt(bb.limit());
writeChar(':');
writeByteBuffer(bb);
}
} else if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> tempMap = (Map<String, Object>) object;
SortedMap<String, Object> tempTree = null;
// unfortunately there are some occasions where we want to ensure that
// the 'key' of the map is not mangled by assuming its UTF-8 encodable.
// In particular the response from a tracker scrape request uses the
// torrent hash as the KEY. Hence the introduction of the type below
// to allow the constructor of the Map to indicate that the keys should
// be extracted using a BYTE_ENCODING
boolean byte_keys = false;//object instanceof ByteEncodedKeyHashMap;
//write the d
writeChar('d');
//are we sorted?
if (tempMap instanceof TreeMap) {
tempTree = (TreeMap<String, Object>) tempMap;
} else {
tempTree = new TreeMap<String, Object>(tempMap);
}
Iterator<Map.Entry<String, Object>> it = tempTree.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) it.next();
Object o_key = entry.getKey();
Object value = entry.getValue();
if (value != null) {
if (o_key instanceof byte[]) {
encodeObject(o_key);
if (!encodeObject(value))
encodeObject("");
} else if (o_key instanceof String) {
String key = (String) o_key;
if (byte_keys) {
try {
encodeObject(Constants.BYTE_CHARSET.encode(key));
if (!encodeObject(value))
encodeObject("");
} catch (UnsupportedEncodingException e) {
throw (new IOException("BEncoder: unsupport encoding: " + e.getMessage()));
}
} else {
// if we put non-ascii chars in as keys we can get horrible expanding
// config issues as we cycle through decode/encode cycles with certain
// characters
encodeObject(key); // Key goes in as UTF-8
if (!encodeObject(value))
encodeObject("");
}
} else
Debug.out("Attempt to encode an unsupported map key type: " + object.getClass() + ";value=" + object);
}
}
writeChar('e');
} else if (object instanceof List) {
@SuppressWarnings("unchecked")
List<Object> tempList = (List<Object>) object;
//write out the l
writeChar('l');
for (int i = 0; i < tempList.size(); i++) {
encodeObject(tempList.get(i));
}
writeChar('e');
} else if (object instanceof Long) {
Long tempLong = (Long) object;
//write out the l
writeChar('i');
writeLong(tempLong.longValue());
writeChar('e');
} else if (object instanceof Integer) {
Integer tempInteger = (Integer) object;
//write out the l
writeChar('i');
writeInt(tempInteger.intValue());
writeChar('e');
} else if (object instanceof byte[]) {
byte[] tempByteArray = (byte[]) object;
writeInt(tempByteArray.length);
writeChar(':');
if (url_encode) {
writeBytes(URLEncoder.encode(new String(tempByteArray, Constants.BYTE_ENCODING), Constants.BYTE_ENCODING).getBytes());
} else {
writeBytes(tempByteArray);
}
} else if (object instanceof ByteBuffer) {
ByteBuffer bb = (ByteBuffer) object;
writeInt(bb.limit());
writeChar(':');
writeByteBuffer(bb);
} else if (object == null) {
// ideally we'd bork here but I don't want to run the risk of breaking existing stuff so just log
Debug.out("Attempt to encode a null value: sofar=" + getEncodedSoFar());
return false;
} else {
Debug.out("Attempt to encode an unsupported entry type: " + object.getClass() + ";value=" + object);
return false;
}
return true;
}
private void writeChar(char c) {
int rem = current_buffer.length - current_buffer_pos;
if (rem > 0) {
current_buffer[current_buffer_pos++] = (byte) c;
} else {
int next_buffer_size = current_buffer.length < BUFFER_DOUBLE_LIMIT ? (current_buffer.length << 1) : (current_buffer.length + BUFFER_DOUBLE_LIMIT);
byte[] new_buffer = new byte[next_buffer_size];
new_buffer[0] = (byte) c;
if (old_buffers == null) {
old_buffers = new byte[][] { current_buffer };
} else {
byte[][] new_old_buffers = new byte[old_buffers.length + 1][];
System.arraycopy(old_buffers, 0, new_old_buffers, 0, old_buffers.length);
new_old_buffers[old_buffers.length] = current_buffer;
old_buffers = new_old_buffers;
}
current_buffer = new_buffer;
current_buffer_pos = 1;
}
}
private void writeInt(int i) {
// we get a bunch of -1 values, optimise
if (i == -1) {
writeBytes(MINUS_1_BYTES);
return;
}
int start = intToBytes(i);
writeBytes(int_buffer, start, 12 - start);
}
private void writeLong(long l) {
if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) {
writeInt((int) l);
} else {
writeBytes(Long.toString(l).getBytes());
}
}
private void writeBytes(byte[] bytes) {
writeBytes(bytes, 0, bytes.length);
}
private void writeBytes(byte[] bytes, int offset, int length) {
int rem = current_buffer.length - current_buffer_pos;
if (rem >= length) {
System.arraycopy(bytes, offset, current_buffer, current_buffer_pos, length);
current_buffer_pos += length;
} else {
if (rem > 0) {
System.arraycopy(bytes, offset, current_buffer, current_buffer_pos, rem);
length -= rem;
}
int next_buffer_size = current_buffer.length < BUFFER_DOUBLE_LIMIT ? (current_buffer.length << 1) : (current_buffer.length + BUFFER_DOUBLE_LIMIT);
byte[] new_buffer = new byte[Math.max(next_buffer_size, length + 512)];
System.arraycopy(bytes, offset + rem, new_buffer, 0, length);
if (old_buffers == null) {
old_buffers = new byte[][] { current_buffer };
} else {
byte[][] new_old_buffers = new byte[old_buffers.length + 1][];
System.arraycopy(old_buffers, 0, new_old_buffers, 0, old_buffers.length);
new_old_buffers[old_buffers.length] = current_buffer;
old_buffers = new_old_buffers;
}
current_buffer = new_buffer;
current_buffer_pos = length;
}
}
private void writeByteBuffer(ByteBuffer bb) {
writeBytes(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining());
}
private String getEncodedSoFar() {
return (new String(toByteArray()));
}
private byte[] toByteArray() {
if (old_buffers == null) {
byte[] res = new byte[current_buffer_pos];
System.arraycopy(current_buffer, 0, res, 0, current_buffer_pos);
// System.out.println( "-> " + current_buffer_pos );
return (res);
} else {
int total = current_buffer_pos;
for (int i = 0; i < old_buffers.length; i++) {
total += old_buffers[i].length;
}
byte[] res = new byte[total];
int pos = 0;
//String str = "";
for (int i = 0; i < old_buffers.length; i++) {
byte[] buffer = old_buffers[i];
int len = buffer.length;
System.arraycopy(buffer, 0, res, pos, len);
pos += len;
//str += (str.length()==0?"":",") + len;
}
System.arraycopy(current_buffer, 0, res, pos, current_buffer_pos);
//System.out.println( "-> " + str + "," + current_buffer_pos );
return (res);
}
}
// JSON
/*
* The following code is from Integer.java as we don't want to
*/
final static byte[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
final static byte[] DigitTens = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', '5',
'5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', };
final static byte[] DigitOnes = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1',
'2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', };
/**
* writes to int_buffer and returns start position in buffer (always runs to end of buffer)
* @param i
* @return
*/
private int intToBytes(int i) {
int q, r;
int charPos = 12;
byte sign = 0;
if (i < 0) {
sign = '-';
i = -i;
}
// Generate two digits per iteration
while (i >= 65536) {
q = i / 100;
// really: r = i - (q * 100);
r = i - ((q << 6) + (q << 5) + (q << 2));
i = q;
int_buffer[--charPos] = DigitOnes[r];
int_buffer[--charPos] = DigitTens[r];
}
// Fall thru to fast mode for smaller numbers
// assert(i <= 65536, i);
for (;;) {
q = (i * 52429) >>> (16 + 3);
r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ...
int_buffer[--charPos] = digits[r];
i = q;
if (i == 0)
break;
}
if (sign != 0) {
int_buffer[--charPos] = sign;
}
return charPos;
}
}