package org.limewire.io;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.limewire.service.ErrorService;
import org.limewire.util.ByteUtils;
import org.limewire.util.NameValue;
import org.limewire.util.StringUtils;
/**
* A mutable GGEP extension block. A GGEP block can be thought of as a
* collection of key/value pairs. A key (extension header) cannot be greater
* than 15 bytes. The value (extension data) can be 0 to 2^24-1 bytes. Values
* can be formatted as a number, boolean, or generic blob of binary data. If
* necessary (e.g., for query replies), GGEP will COBS-encode values to remove
* null bytes. The order of the extensions is immaterial. Extensions supported
* by LimeWire have keys specified in this class (prefixed by GGEP_HEADER...)
*/
public class GGEP {
/** The maximum size of a extension header (key). */
public static final int MAX_KEY_SIZE_IN_BYTES = 15;
/** The maximum size of a extension data (value). */
public static final int MAX_VALUE_SIZE_IN_BYTES = 262143;
/** The GGEP prefix. A GGEP block will start with this byte value.
*/
public static final byte GGEP_PREFIX_MAGIC_NUMBER = (byte) 0xC3;
/**
* Default character set for GGEP values encoded as Strings
*/
private static final String DEFAULT_ENCODING_CHARSET = "UTF-8";
/**
* The collection of key/value pairs. Rep. rationale: arrays of bytes are
* convenient for values since they're easy to convert to numbers or
* strings. But strings are convenient for keys since they define hashCode
* and equals.
*/
private final Map<String, Object> _props = new TreeMap<String, Object>();
/** True if COBS encoding is required. */
private final boolean useCOBS;
/**
* Cached hash code value to avoid calculating the hash code from the
* map each time.
*/
private volatile int hashCode = 0;
//////////////////// Encoding/Decoding (Map <==> byte[]) ///////////////////
/**
* Creates a new empty GGEP block. Typically this is used for outgoing
* messages and mutated before encoding.
*
* @param useCOBS false if nulls are allowed in extension values;true if
* this should activate COBS encoding if necessary to remove null bytes.
*/
public GGEP(boolean useCOBS) {
this.useCOBS = useCOBS;
}
/** Creates a new empty GGEP block that does not needs COBS encoding. */
public GGEP() {
this(false);
}
/**
* Constructs a new ggep message with the given data.
*/
public GGEP(byte[] data) throws BadGGEPBlockException {
this(data, 0);
}
/**
* Constructs a new GGEP message with the given bytes & offset.
*/
public GGEP(byte[] data, int offset) throws BadGGEPBlockException {
this(data, offset, null);
}
/**
* Constructs a GGEP instance based on the GGEP block beginning at
* messageBytes[beginOffset].
* @param messageBytes the bytes of the message.
* @param beginOffset the begin index of the GGEP prefix.
* @param endOffset if you want to get the offset where the GGEP block
* ends (more precisely, one above the ending index), then send me a
* int[1]. I'll put the endOffset in endOffset[0]. If you don't care,
* null will do....
* @exception BadGGEPBlockException Thrown if the block could not be parsed
* correctly.
*/
public GGEP(byte[] messageBytes, final int beginOffset, int[] endOffset)
throws BadGGEPBlockException {
if (messageBytes.length - beginOffset < 4)
throw new BadGGEPBlockException();
// all GGEP blocks start with this prefix....
if (messageBytes[beginOffset] != GGEP_PREFIX_MAGIC_NUMBER)
throw new BadGGEPBlockException();
boolean tUseCOBS = false;
boolean onLastExtension = false;
int currIndex = beginOffset + 1;
while (!onLastExtension) {
// process extension header flags
// bit order is interpreted as 76543210
try {
sanityCheck(messageBytes[currIndex]);
} catch (ArrayIndexOutOfBoundsException malformedInput) {
throw new BadGGEPBlockException();
}
onLastExtension = isLastExtension(messageBytes[currIndex]);
boolean encoded = isEncoded(messageBytes[currIndex]);
boolean compressed = isCompressed(messageBytes[currIndex]);
int headerLen = deriveHeaderLength(messageBytes[currIndex]);
// get the extension header
currIndex++;
String extensionHeader = null;
try {
extensionHeader = StringUtils.getASCIIString(messageBytes, currIndex,
headerLen);
} catch (StringIndexOutOfBoundsException inputIsMalformed) {
throw new BadGGEPBlockException();
}
// get the data length
currIndex += headerLen;
int[] toIncrement = new int[1];
final int dataLength = deriveDataLength(messageBytes, currIndex,
toIncrement);
byte[] extensionData = null;
currIndex+=toIncrement[0];
if (dataLength > 0) {
// ok, data is present, get it....
byte[] data = new byte[dataLength];
try {
System.arraycopy(messageBytes, currIndex, data, 0,
dataLength);
} catch (ArrayIndexOutOfBoundsException malformedInput) {
throw new BadGGEPBlockException();
}
if (encoded) {
tUseCOBS = true;
try {
data = GGEP.cobsDecode(data);
} catch (IOException badCobsEncoding) {
throw new BadGGEPBlockException("Bad COBS Encoding");
}
}
if (compressed) {
try {
data = IOUtils.inflate(data);
} catch(IOException badData) {
throw new BadGGEPBlockException("Bad compressed data");
}
}
extensionData = data;
currIndex += dataLength;
}
// ok, everything checks out, just slap it in the hashmapper...
if(compressed)
_props.put(extensionHeader, new NeedsCompression(extensionData));
else
_props.put(extensionHeader, extensionData);
}
if ((endOffset != null) && (endOffset.length > 0))
endOffset[0] = currIndex;
useCOBS = tUseCOBS;
}
/**
* Merges the other's GGEP with this' GGEP.
*/
public void merge(GGEP other) {
_props.putAll(other._props);
}
private void sanityCheck(byte headerFlags) throws BadGGEPBlockException {
// the 4th bit in the header's first byte must be 0.
if ((headerFlags & 0x10) != 0)
throw new BadGGEPBlockException();
}
private boolean isLastExtension(byte headerFlags) {
boolean retBool = false;
// the 8th bit in the header's first byte, when set, indicates that
// this header is the last....
if ((headerFlags & 0x80) != 0)
retBool = true;
return retBool;
}
private boolean isEncoded(byte headerFlags) {
boolean retBool = false;
// the 7th bit in the header's first byte, when set, indicates that
// this header is the encoded with COBS
if ((headerFlags & 0x40) != 0)
retBool = true;
return retBool;
}
private boolean isCompressed(byte headerFlags) {
boolean retBool = false;
// the 6th bit in the header's first byte, when set, indicates that
// this header is the compressed with deflate
if ((headerFlags & 0x20) != 0)
retBool = true;
return retBool;
}
private int deriveHeaderLength(byte headerFlags)
throws BadGGEPBlockException {
int retInt = 0;
// bits 0-3 give the length of the extension header (1-15)
retInt = headerFlags & 0x0F;
if (retInt == 0)
throw new BadGGEPBlockException();
return retInt;
}
/** @param increment a int array of size >0. i'll put the number of bytes
* devoted to data storage in increment[0].
*/
private int deriveDataLength(byte[] buff, int beginOffset, int increment[])
throws BadGGEPBlockException {
int length = 0, iterations = 0;
// the length is stored in at most 3 bytes....
final int MAX_ITERATIONS = 3;
byte currByte;
do {
try {
currByte = buff[beginOffset++];
}
catch (ArrayIndexOutOfBoundsException malformedInput) {
throw new BadGGEPBlockException();
}
length = (length << 6) | (currByte & 0x3f);
if (++iterations > MAX_ITERATIONS)
throw new BadGGEPBlockException();
} while (0x40 != (currByte & 0x40));
increment[0] = iterations;
return length;
}
/** Writes this GGEP instance as a properly formatted GGEP Block.
* @param out this GGEP instance is written to out.
* @exception IOException thrown if had error writing to out.
*/
public void write(OutputStream out) throws IOException {
if (getHeaders().size() > 0) {
// start with the magic prefix
out.write(GGEP_PREFIX_MAGIC_NUMBER);
Iterator<String> headers = getHeaders().iterator();
// for each header, write the GGEP header and data
while (headers.hasNext()) {
String currHeader = headers.next();
byte[] currData = get(currHeader);
int dataLen = 0;
boolean shouldEncode = shouldCOBSEncode(currData);
boolean shouldCompress = shouldCompress(currHeader);
if (currData != null) {
if (shouldCompress) {
currData = IOUtils.deflate(currData);
if(currData.length > MAX_VALUE_SIZE_IN_BYTES)
throw new IllegalArgumentException("value for ["
+ currHeader + "] too large after compression");
} if (shouldEncode)
currData = GGEP.cobsEncode(currData);
dataLen = currData.length;
}
writeHeader(currHeader, dataLen,
!headers.hasNext(), out,
shouldEncode, shouldCompress);
if (dataLen > 0)
out.write(currData);
}
}
}
/**
* Returns the GGEP as a byte array.
* @return an empty array if GGEP is empty
*/
public byte[] toByteArray() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
write(out);
} catch (IOException e) {
ErrorService.error(e);
}
return out.toByteArray();
}
private final boolean shouldCOBSEncode(byte[] data) {
// if nulls are allowed from construction time and if nulls are present
// in the data...
return (useCOBS && containsNull(data));
}
private final boolean shouldCompress(String header) {
return (_props.get(header) instanceof NeedsCompression);
}
private void writeHeader(String header, final int dataLen,
boolean isLast, OutputStream out,
boolean isEncoded, boolean isCompressed)
throws IOException {
// 1. WRITE THE HEADER FLAGS
byte[] headerBytes = StringUtils.toAsciiBytes(header);
int flags = 0x00;
if (isLast)
flags |= 0x80;
if (isEncoded)
flags |= 0x40;
if (isCompressed)
flags |= 0x20;
flags |= headerBytes.length;
out.write(flags);
// 2. WRITE THE HEADER
out.write(headerBytes);
// 3. WRITE THE DATA LEN
// possibly 3 bytes
int toWrite;
int begin = dataLen & 0x3F000;
if (dataLen > 0x00000fff) {
begin = begin >> 12; // relevant bytes at the bottom now...
toWrite = 0x80 | begin;
out.write(toWrite);
}
int middle = dataLen & 0xFC0;
if (dataLen > 0x0000003f) {
middle = middle >> 6; // relevant bytes at the bottom now...
toWrite = 0x80 | middle;
out.write(toWrite);
}
int end = dataLen & 0x3F; // shut off everything except last 6 bits...
toWrite = 0x40 | end;
out.write(toWrite);
}
/**
* Returns the amount of overhead that will be added
* when the following key/value pair is written.
* <p>
* This does *NOT* work for non-ASCII headers, or compressed data.
*/
public int getHeaderOverhead(String key) {
byte[] data = get(key);
if(data == null)
throw new IllegalArgumentException("no data for key: " + key);
return 1 + // flags
key.length() + // header
data.length + // data
1 + // required data length
(data.length > 0x3F ? 1 : 0) + // optional data
(data.length > 0xFFF ? 1 : 0); // more option data
}
////////////////////////// Key/Value Mutators and Accessors ////////////////
/**
* Adds all the specified key/value pairs.
*/
/* TODO: Allow a value to be compressed.
*/
public void putAll(List<? extends NameValue<?>> fields) throws IllegalArgumentException {
for(NameValue<?> next : fields) {
String key = next.getName();
Object value = next.getValue();
if(value == null)
put(key);
else if(value instanceof byte[])
put(key, (byte[])value);
else if(value instanceof String)
put(key, (String)value);
else if(value instanceof Integer)
put(key, ((Integer)value).intValue());
else if(value instanceof Long)
put(key, ((Long)value).longValue());
else if(value instanceof Byte)
put(key, ((Byte)value).byteValue());
else
throw new IllegalArgumentException("Unknown value: " + value);
}
}
/**
* Adds a key with data that should be compressed.
*/
public void putCompressed(String key, byte[] value) throws IllegalArgumentException {
validateKey(key);
if(value == null)
throw new IllegalArgumentException("null value for key: " + key);
//validateValue(value); // done when writing. TODO: do here?
_props.put(key, new NeedsCompression(value));
}
/**
* Adds a key with byte value.
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @param value the GGEP extension data.
*/
public void put(String key, byte value) throws IllegalArgumentException {
put(key, new byte[] { value } );
}
/**
* Adds a key with raw byte value.
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @param value the GGEP extension data
* @exception IllegalArgumentException key is of an illegal length
* or if value is null.
*/
public void put(String key, byte[] value) throws IllegalArgumentException {
validateKey(key);
validateValue(value, key);
_props.put(key, value);
}
/**
* Adds a key with string value, using the default character encoding.
* <p>
* Enforcing a default encoding (UTF-8) because each machine can have its
* own default encoding
*
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @param value the GGEP extension data
* @exception IllegalArgumentException key is of an illegal length
* or if value is null
*/
public void put(String key, String value) throws IllegalArgumentException {
put(key, value, DEFAULT_ENCODING_CHARSET);
}
/**
* Keeping private access modifier until necessary to
* treat GGEP value Strings as encoded in different character sets.
*/
private void put(String key, String value, String charSetName)
throws IllegalArgumentException {
try {
put(key, value==null ? null : value.getBytes(charSetName));
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Unsupported character set for " +
"String encoding", e);
}
}
/**
* Adds a key with integer value.
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @param value the GGEP extension data, which should be an unsigned integer
* @exception IllegalArgumentException key is of an illegal length
* or if value is negative
*/
public void put(String key, int value) throws IllegalArgumentException {
if (value < 0) // int2minLeb doesn't work on negative values
throw new IllegalArgumentException("Negative value: " + value + " for key: " + key);
put(key, ByteUtils.int2minLeb(value));
}
/**
* Adds a key with long value.
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @param value the GGEP extension data, which should be an unsigned long
* @exception IllegalArgumentException key is of an illegal length
* of if value is negative
*/
public void put(String key, long value) throws IllegalArgumentException {
if (value < 0) // long2minLeb doesn't work on negative values
throw new IllegalArgumentException("Negative value: " + value + " for key: " + key);
put(key, ByteUtils.long2minLeb(value));
}
/**
* Adds a key without any value.
* @param key the name of the GGEP extension, whose length should be between
* 1 and 15, inclusive
* @exception IllegalArgumentException key is of an illegal length.
*/
public void put(String key) throws IllegalArgumentException {
validateKey(key);
_props.put(key, null);
}
/**
* Returns the value for a key, as raw bytes.
* @param key the name of the GGEP extension
* @return the GGEP extension data associated with the key
* @exception BadGGEPPropertyException extension not found, was corrupt,
* or has no associated data. Note that BadGGEPPropertyException is
* is always thrown for extensions with no data; use hasKey instead.
*/
public byte[] getBytes(String key) throws BadGGEPPropertyException {
byte[] ret= get(key);
if (ret==null)
throw new BadGGEPPropertyException();
return ret;
}
/**
* Returns the value for a key, as a string, using the default encoding (UTF-8).
*
* @param key the name of the GGEP extension
* @return the GGEP extension data associated with the key
* @exception BadGGEPPropertyException extension not found, was corrupt,
* or has no associated data. Note that BadGGEPPropertyException is
* is always thrown for extensions with no data; use hasKey instead.
*/
public String getString(String key) throws BadGGEPPropertyException {
return getString(key, DEFAULT_ENCODING_CHARSET);
}
/**
* Keeping private access modifier until necessary to
* treat GGEP value Strings as encoded in different character sets.
*/
private String getString(String key, String encoding) throws BadGGEPPropertyException, IllegalArgumentException {
try {
return new String(getBytes(key), encoding);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Cannot get GGEP key value as " +
"String due to unsupported encoding", e);
}
}
/**
* Returns the value for a key, as an integer.
* @param key the name of the GGEP extension
* @return the GGEP extension data associated with the key
* @exception BadGGEPPropertyException extension not found, was corrupt,
* or has no associated data. Note that BadGGEPPropertyException is
* is always thrown for extensions with no data; use hasKey instead.
*/
public int getInt(String key) throws BadGGEPPropertyException {
byte[] bytes=getBytes(key);
if (bytes.length<1)
throw new BadGGEPPropertyException("No bytes");
if (bytes.length>4)
throw new BadGGEPPropertyException("Integer too big");
return ByteUtils.leb2int(bytes, 0, bytes.length);
}
/**
* Returns the value for a key as a long.
* @param key the name of the GGEP extension
* @return the GGEP extension data associated with the key
* @exception BadGGEPPropertyException extension not found, was corrupt,
* or has no associated data. Note that BadGGEPPropertyException is
* is always thrown for extensions with no data; use hasKey instead.
*/
public long getLong(String key) throws BadGGEPPropertyException {
byte[] bytes=getBytes(key);
if (bytes.length<1)
throw new BadGGEPPropertyException("No bytes");
if (bytes.length>8)
throw new BadGGEPPropertyException("Integer too big");
return ByteUtils.leb2long(bytes, 0, bytes.length);
}
/**
* Returns whether this has the given key.
* @param key the name of the GGEP extension
* @return true if this has a key
*/
public boolean hasKey(String key) {
return _props.containsKey(key);
}
/** Returns true if the GGEP has a non-null value for the key. */
public boolean hasValueFor(String key) {
return get(key) != null;
}
/**
* Returns the set of keys.
* @return a set of all the GGEP extension header name in this, each
* as a String.
*/
public Set<String> getHeaders() {
return _props.keySet();
}
/**
* Returns whether this GGEP is empty or not.
*/
public boolean isEmpty() {
return _props.isEmpty();
}
/**
* Gets the byte[] data from props.
*/
public byte[] get(String key) {
Object value = _props.get(key);
if(value instanceof NeedsCompression)
return ((NeedsCompression)value).data;
else
return (byte[])value;
}
private void validateKey(String key) throws IllegalArgumentException {
if (!StringUtils.isAsciiOnly(key)) {
throw new IllegalArgumentException("key is not ascii only: " + key);
}
byte[] bytes = StringUtils.toAsciiBytes(key);
if ( key.equals("")
|| (bytes.length > MAX_KEY_SIZE_IN_BYTES)
|| containsNull(bytes))
throw new IllegalArgumentException("invalid key: " + key);
}
private void validateValue(byte[] value, String key) throws IllegalArgumentException {
if (value==null)
throw new IllegalArgumentException("null value for key: " + key);
if (value.length>MAX_VALUE_SIZE_IN_BYTES)
throw new IllegalArgumentException("value (" + value + ") too large for key: " + key);
}
private boolean containsNull(byte[] bytes) {
if (bytes != null) {
for (int i = 0; i < bytes.length; i++)
if (bytes[i] == 0x0)
return true;
}
return false;
}
//////////////////////////////// Miscellany ///////////////////////////////
/** @return True if the two Maps that represent header/data pairs are
* equivalent.
*/
@Override
public boolean equals(Object o) {
if(o == this) return true;
if (! (o instanceof GGEP))
return false;
//This is O(n lg n) time with n keys. It would be great if we could
//just check that the trees are isomorphic. I don't think this code is
//really used anywhere, however.
return this.subset((GGEP)o) && ((GGEP)o).subset(this);
}
/** Returns true if this is a subset of other, e.g., all of this' keys
* can be found in OTHER with the same value. */
private boolean subset(GGEP other) {
for(String key : _props.keySet()) {
byte[] v1= this.get(key);
byte[] v2= other.get(key);
//Remember that v1 and v2 can be null.
if ((v1==null) != (v2==null))
return false;
if (v1!=null && !Arrays.equals(v1, v2))
return false;
}
return true;
}
// overrides Object.hashCode to be consistent with equals
@Override
public int hashCode() {
if(hashCode == 0) {
hashCode = 37 * _props.hashCode();
}
return hashCode;
}
/* COBS implementation....
* For implementation details, please see:
* http://www.acm.org/sigcomm/sigcomm97/papers/p062.pdf
*/
/** Decode a COBS-encoded byte array. The non-allowable byte value is 0.<p>
* PRE: src is not null.<p>
* POST: the return array will be a cobs decoded version of src. namely,
* cobsDecode(cobsEncode(src)) == src.
* @return the original COBS decoded string
*/
static byte[] cobsDecode(byte[] src) throws IOException {
final int srcLen = src.length;
int currIndex = 0;
int code = 0;
ByteArrayOutputStream sink = new ByteArrayOutputStream();
while (currIndex < srcLen) {
code = ByteUtils.ubyte2int(src[currIndex++]);
if ((currIndex+(code-2)) >= srcLen)
throw new IOException();
for (int i = 1; i < code; i++) {
sink.write(src[currIndex++]);
}
if (currIndex < srcLen) // don't write this last one, it isn't used
if (code < 0xFF) sink.write(0);
}
return sink.toByteArray();
}
static int cobsFinishBlock(int code, ByteArrayOutputStream sink,
byte[] src, int begin, int end) {
sink.write(code);
if (begin > -1)
sink.write(src, begin, (end-begin)+1);
return (byte) 0x01;
}
/** Encode a byte array with COBS. The non-allowable byte value is 0.<p>
* PRE: src is not null.<p>
* POST: the return array will be a cobs encoded version of src. namely,
* cobsDecode(cobsEncode(src)) == src.
* @return a COBS encoded version of src.
*/
static byte[] cobsEncode(byte[] src) {
final int srcLen = src.length;
int code = 1;
int currIndex = 0;
// COBS encoding adds no more than one byte of overhead for every 254
// bytes of packet data
final int maxEncodingLen = src.length + ((src.length+1)/254) + 1;
ByteArrayOutputStream sink = new ByteArrayOutputStream(maxEncodingLen);
int writeStartIndex = -1;
while (currIndex < srcLen) {
if (src[currIndex] == 0) {
// currIndex was incremented so take 1 less
code = GGEP.cobsFinishBlock(code, sink, src, writeStartIndex,
(currIndex-1));
writeStartIndex = -1;
}
else {
if (writeStartIndex < 0) writeStartIndex = currIndex;
code++;
if (code == 0xFF) {
code = GGEP.cobsFinishBlock(code, sink, src, writeStartIndex,
currIndex);
writeStartIndex = -1;
}
}
currIndex++;
}
// currIndex was incremented so take 1 less
GGEP.cobsFinishBlock(code, sink, src, writeStartIndex, (currIndex-1));
return sink.toByteArray();
}
/**
* Marker class that wraps a byte[] value, if that value
* is going to require compression upon write.
*/
private static class NeedsCompression {
final byte[] data;
NeedsCompression(byte[] data) {
this.data = data;
}
}
}