/*
* 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 org.gudy.azureus2.core3.util;
import java.io.*;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.*;
import org.gudy.azureus2.core3.xml.util.XUXmlWriter;
/**
* 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
*/
public 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 object )
throws IOException
{
return( encode( object, false ));
}
public static byte[]
encode(
Map 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 void
encodeObject(
Object object)
throws IOException
{
if ( object instanceof String || object instanceof Float){
String tempString = (object instanceof String) ? (String)object : String.valueOf((Float)object);
ByteBuffer bb = Constants.DEFAULT_CHARSET.encode( tempString );
writeInt( bb.limit() );
writeChar(':');
writeByteBuffer(bb );
}else if(object instanceof Map){
Map tempMap = (Map)object;
SortedMap 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 = object instanceof ByteEncodedKeyHashMap;
//write the d
writeChar('d');
//are we sorted?
if ( tempMap instanceof TreeMap ){
tempTree = (TreeMap)tempMap;
}else{
tempTree = new TreeMap(tempMap);
}
Iterator it = tempTree.entrySet().iterator();
while( it.hasNext()){
Map.Entry entry = (Map.Entry)it.next();
Object o_key = entry.getKey();
Object value = entry.getValue();
if ( value != null ){
if ( o_key instanceof byte[]){
encodeObject( o_key );
encodeObject( value );
}else{
String key = (String)o_key;
if ( byte_keys ){
try{
encodeObject( Constants.BYTE_CHARSET.encode(key));
encodeObject( tempMap.get(key));
}catch( UnsupportedEncodingException e ){
throw( new IOException( "BEncoder: unsupport encoding: " + e.getMessage()));
}
}else{
encodeObject( key ); // Key goes in as UTF-8
encodeObject( value);
}
}
}
}
writeChar('e');
}else if(object instanceof List){
List tempList = (List)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" );
}else{
Debug.out( "Attempt to encode an unsupported entry type: " + object.getClass() + ";value=" + object);
}
}
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 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 );
}
}
private static Object
normaliseObject(
Object o )
{
if ( o instanceof Integer ){
o = new Long(((Integer)o).longValue());
}else if ( o instanceof Boolean ){
o = new Long(((Boolean)o).booleanValue()?1:0);
}else if ( o instanceof Float ){
o = String.valueOf((Float)o);
}else if ( o instanceof byte[] ){
try{
o = new String((byte[])o,"UTF-8");
}catch( Throwable e ){
}
}
return( o );
}
public static boolean
objectsAreIdentical(
Object o1,
Object o2 )
{
if ( o1 == null && o2 == null ){
return( true );
}else if ( o1 == null || o2 == null ){
return( false );
}
if ( o1.getClass() != o2.getClass()){
if ( ( o1 instanceof Map && o2 instanceof Map ) ||
( o1 instanceof List && o2 instanceof List )){
// things actually OK
}else{
o1 = normaliseObject( o1 );
o2 = normaliseObject( o2 );
if ( o1.getClass() != o2.getClass()){
Debug.out( "Failed to normalise classes " + o1.getClass() + "/" + o2.getClass());
return( false );
}
}
}
if ( o1 instanceof Long ||
o1 instanceof String ){
return( o1.equals( o2 ));
}else if ( o1 instanceof byte[] ){
return( Arrays.equals((byte[])o1,(byte[])o2 ));
}else if ( o1 instanceof List ){
return( listsAreIdentical((List)o1,(List)o2));
}else if ( o1 instanceof Map ){
return( mapsAreIdentical((Map)o1,(Map)o2));
}else if ( o1 instanceof Integer ||
o1 instanceof Boolean ||
o1 instanceof Float ||
o1 instanceof ByteBuffer ){
return( o1.equals( o2 ));
}else{
Debug.out( "Invalid type: " + o1 );
return( false );
}
}
public static boolean
listsAreIdentical(
List list1,
List list2 )
{
if ( list1 == null && list2 == null ){
return( true );
}else if ( list1 == null || list2 == null ){
return( false );
}
if ( list1.size() != list2.size()){
return( false );
}
for ( int i=0;i<list1.size();i++){
if ( !objectsAreIdentical( list1.get(i), list2.get(i))){
return( false );
}
}
return( true );
}
public static boolean
mapsAreIdentical(
Map map1,
Map map2 )
{
if ( map1 == null && map2 == null ){
return( true );
}else if ( map1 == null || map2 == null ){
return( false );
}
if ( map1.size() != map2.size()){
return( false );
}
Iterator it = map1.keySet().iterator();
while( it.hasNext()){
Object key = it.next();
Object v1 = map1.get(key);
Object v2 = map2.get(key);
if ( !objectsAreIdentical( v1, v2 )){
return( false );
}
}
return( true );
}
public static Map
cloneMap(
Map map )
{
if ( map == null ){
return( null );
}
Map res = new TreeMap();
Iterator it = map.entrySet().iterator();
while( it.hasNext()){
Map.Entry entry = (Map.Entry)it.next();
Object key = entry.getKey();
Object value = entry.getValue();
// keys must be String (or very rarely byte[])
if ( key instanceof byte[] ){
key = ((byte[])key).clone();
}
res.put( key, clone( value ));
}
return( res );
}
public static List
cloneList(
List list )
{
if ( list == null ){
return( null );
}
List res = new ArrayList(list.size());
Iterator it = list.iterator();
while( it.hasNext()){
res.add( clone( it.next()));
}
return( res );
}
public static Object
clone(
Object obj )
{
if ( obj instanceof List ){
return( cloneList((List)obj));
}else if ( obj instanceof Map ){
return( cloneMap((Map)obj));
}else if ( obj instanceof byte[]){
return(((byte[])obj).clone());
}else{
// assume immutable - String,Long etc
return( obj );
}
}
public static StringBuffer
encodeToXML(
Map map,
boolean simple )
{
XMLEncoder writer = new XMLEncoder();
return( writer.encode( map, simple ));
}
/*
* 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;
}
protected static class
XMLEncoder
extends XUXmlWriter
{
protected
XMLEncoder()
{
}
protected StringBuffer
encode(
Map map,
boolean simple )
{
StringWriter writer = new StringWriter(1024);
setOutputWriter( writer );
setGenericSimple( simple );
writeGeneric( map );
flushOutputStream();
return( writer.getBuffer());
}
}
}