package com.limegroup.gnutella.dime;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.util.DataUtils;
/**
* Class holding a DIMERecord as part of a DIME Message.
*
* @author Gregorio Roper
* @author Sam Berlin
*/
public class DIMERecord {
private static final Log LOG = LogFactory.getLog(DIMERecord.class);
// A DIME Record looks like the following:
///////////////////////////////////////////////////////////////////
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
// ----------------------------------------------------------------
// VERSION |M|M|C| TYPE | RSRV | OPTIONS_LENGTH
// |B|E|F| | |
// ----------------------------------------------------------------
// ID_LENGTH | TYPE_LENGTH
// ----------------------------------------------------------------
// DATA_LENGTH
// ----------------------------------------------------------------
// OPTIONS + PADDING
// ----------------------------------------------------------------
// ID + PADDING
// ----------------------------------------------------------------
// TYPE + PADDING
// ----------------------------------------------------------------
// DATA + PADDING
// ----------------------------------------------------------------
///////////////////////////////////////////////////////////////////
// Where padding brings the field equal to a multiple octects.
// There must not be more than 3 octects of padding.
// All integer fields (anything ending in _LENGTH) are in BIG ENDIAN
// format.
// The header is considered to be bytes 0-12 (up to the end of DATA_LENGTH)
// since all DIMERecords must contain atleast those 12 bytes.
// For the particulars of DIME, see: http://www.perfectxml.com/DIME.asp
/**
* The current (and only) version of a DIME Record.
*/
public static final byte VERSION = 0x01 << 3;
/**
* The version mask.
*/
private static final byte VERSION_MASK = (byte)0xF8;
/**
* The mask marking this is the first record in a dime message.
*/
private static final byte MB_MASK = 0x01 << 2;
/**
* The mask marking this as the last record in a dime message.
*/
private static final byte ME_MASK = 0x01 << 1;
/**
* The mark marking this as a chunked record (set in the first
* and all subsequent records except for the very last one) in a DIME
* message.
*/
private static final byte CF_MASK = 0x01;
/**
* The first byte of the message, containing the version, mb, me, and cf.
*/
private byte _byte1;
/**
* The flag representing the UNCHANGED type.
*
* This means to use the type of the previous record. It is used by
* all chunked records (beginning with the 2nd chunk) and requires that
* the TYPE_LENGTH be 0.
*/
public static final byte TYPE_UNCHANGED = 0x0;
/**
* The flag representing the MEDIA_TYPE type.
*
* This means the type is a Media Type as defined by RFC 2616, described
* at http://www.ietf.org/rfc/rfc2616.txt in section 3.7.
*/
public static final byte TYPE_MEDIA_TYPE = 0x01 << 4;
/**
* The flag representing an absolute URI.
*/
public static final byte TYPE_ABSOLUTE_URI = 0x02 << 4;
/**
* The flag representing an unknown type.
*/
public static final byte TYPE_UNKNOWN = 0x03 << 4;
/**
* The flag representing no type.
*/
public static final byte TYPE_NONE = 0x04 << 4;
/**
* The type mask.
*/
private static final byte TYPE_MASK = (byte)0xF0;
/**
* The reserved value. Must be 0 in a valid DIME record.
*/
private static final byte RESERVED = 0x0;
/**
* The reserved mask.
*/
private static final byte RESERVED_MASK = 0xF;
/**
* The second byte, containing the type & reserved flag.
*/
private final byte _byte2;
/**
* The options.
*/
private final byte[] _options;
/**
* The ID.
*/
private final byte[] _id;
/**
* The type.
*/
private final byte[] _type;
/**
* The data.
*/
private final byte[] _data;
/**
* The ID as a string.
*/
private String _idString = null;
/**
* A Map of the options.
*/
private Map _optionsMap = null;
/**
* Constructs a new DIMERecord with the given data.
*/
public DIMERecord(byte byte1, byte byte2, byte[] options,
byte[] id, byte[] type, byte[] data) {
_byte1 = byte1;
_byte2 = byte2;
if(options == null)
options = DataUtils.EMPTY_BYTE_ARRAY;
if(id == null)
id = DataUtils.EMPTY_BYTE_ARRAY;
if(type == null)
type = DataUtils.EMPTY_BYTE_ARRAY;
if(data == null)
data = DataUtils.EMPTY_BYTE_ARRAY;
_options = options;
_id = id;
_type = type;
_data = data;
validate();
}
/**
* Constructs a new DIMERecord with the given information.
*/
public DIMERecord(byte typeId, byte[] options, byte[] id,
byte[] type, byte[] data) {
this(VERSION, (byte)(typeId | RESERVED),
options, id, type, data);
}
/**
* Constructs a new DIMERecord from an InputStream.
*/
public static DIMERecord createFromStream(InputStream in) throws IOException {
byte[] header = new byte[12];
fillBuffer(header, in);
try {
validateFirstBytes(header[0], header[1]);
} catch(IllegalArgumentException iae) {
throw new IOException(iae.getMessage());
}
int optionsLength = ByteOrder.beb2int(header, 2, 2);
int idLength = ByteOrder.beb2int(header, 4, 2);
int typeLength = ByteOrder.beb2int(header, 6, 2);
int dataLength = ByteOrder.beb2int(header, 8, 4);
if(LOG.isDebugEnabled()) {
LOG.debug("creating dime record." +
" optionsLength: " + optionsLength +
", idLength: " + idLength +
", typeLength: " + typeLength +
", dataLength: " + dataLength);
}
//The DIME specification allows this to be a 32-bit unsigned field,
//which in Java would be a long -- but in order to hold the array
//of the data, we can only read up to 16 unsigned bits (an int), in order
//to size the array correctly.
if(dataLength < 0)
throw new IOException("data too big.");
byte[] options = readInformation(optionsLength, in);
byte[] id = readInformation(idLength, in);
byte[] type = readInformation(typeLength, in);
byte[] data = readInformation(dataLength, in);
try {
return new DIMERecord(header[0], header[1],
options, id, type, data);
} catch(IllegalArgumentException iae) {
throw new IOException(iae.getMessage());
}
}
/**
* Determines the length of the full record.
*/
public int getRecordLength() {
return 12 // header
+ getOptionsLength() + calculatePaddingLength(getOptionsLength())
+ getIdLength() + calculatePaddingLength(getIdLength())
+ getTypeLength() + calculatePaddingLength(getTypeLength())
+ getDataLength() + calculatePaddingLength(getDataLength());
}
/**
* Writes this record to the given OutputStream.
*/
void write(OutputStream out) throws IOException {
// Write the header.
out.write(_byte1);
out.write(_byte2);
ByteOrder.int2beb(getOptionsLength(), out, 2);
ByteOrder.int2beb(getIdLength(), out, 2);
ByteOrder.int2beb(getTypeLength(), out, 2);
ByteOrder.int2beb(getDataLength(), out, 4);
// Write out the data.
writeOptions(out);
writeId(out);
writeType(out);
writeData(out);
}
/**
* Writes the option out.
*/
public void writeOptions(OutputStream out) throws IOException {
writeDataWithPadding(_options, out);
}
/**
* Writes the id out.
*/
public void writeId(OutputStream out) throws IOException {
writeDataWithPadding(_id, out);
}
/**
* Writes the type out.
*/
public void writeType(OutputStream out) throws IOException {
writeDataWithPadding(_type, out);
}
/**
* Writes the data out.
*/
public void writeData(OutputStream out) throws IOException {
writeDataWithPadding(_data, out);
}
/**
* Sets this to be the first record in a sequence of records.
*/
public void setFirstRecord(boolean first) {
if(first)
_byte1 |= MB_MASK;
else
_byte1 &= ~MB_MASK;
}
/**
* Determines is this record is the first in a series of records.
*/
public boolean isFirstRecord() {
return (_byte1 & MB_MASK) == MB_MASK;
}
/**
* Sets this to be the last record in a sequence of records.
*/
public void setLastRecord(boolean last) {
if(last)
_byte1 |= ME_MASK;
else
_byte1 &= ~ME_MASK;
}
/**
* Determines if this record is the last in a series of records.
*/
public boolean isLastRecord() {
return (_byte1 & ME_MASK) == ME_MASK;
}
/**
* Returns one of the type constants:
* TYPE_UNCHANGED
* TYPE_MEDIA_TYPE
* TYPE_ABSOLUTE_URI
* TYPE_UNKNOWN
* TYPE_NONE
*/
public int getTypeId() {
return _byte2 & TYPE_MASK;
}
/**
* Returns the length of the type.
*/
public int getTypeLength() {
return _type.length;
}
/**
* @return typeField of <tt>DIMERecord</tt>
*/
public byte[] getType() {
return _type;
}
/**
* @return String representation of type field
*/
public String getTypeString() {
try {
return new String(getType(), "UTF-8");
} catch (UnsupportedEncodingException e) {
ErrorService.error(e);
return null;
}
}
/**
* Returns the length of the data.
*/
public int getDataLength() {
return _data.length;
}
/**
* @return dataField of <tt>DIMERecord</tt>
*/
public byte[] getData() {
return _data;
}
/**
* Returns the length of the id.
*/
public int getIdLength() {
return _id.length;
}
/**
* @return idField of <tt>DIMERecord</tt>
*/
public byte[] getId() {
return _id;
}
/**
* Returns the length of the options.
*/
public int getOptionsLength() {
return _options.length;
}
/**
* @return optionsField of <tt>DIMERecord</tt>
*/
public byte[] getOptions() {
return _options;
}
/**
* @return String containing the URI for this DIMERecord
*/
public String getIdentifier() {
if (_idString == null)
_idString = new String(getId());
return _idString;
}
/**
* @return Map of String->String
*
* @throws DIMEMessageException
* in case of a problem reading the message
*/
public Map getOptionsMap() throws DIMEMessageException {
if (_optionsMap == null)
_optionsMap = parseOptions(getOptions());
return _optionsMap;
}
/**
* Writes the padding necessary for the given length.
*/
public static void writePadding(int length, OutputStream os)
throws IOException {
// write the padding.
int padding = calculatePaddingLength(length);
switch(padding) {
case 0:
return;
case 1:
os.write(DataUtils.BYTE_ARRAY_ONE);
return;
case 2:
os.write(DataUtils.BYTE_ARRAY_TWO);
return;
case 3:
os.write(DataUtils.BYTE_ARRAY_THREE);
return;
default:
throw new IllegalStateException("invalid padding.");
}
}
/**
* Validates the first two bytes.
*/
private static void validateFirstBytes(byte one, byte two) {
if((one & VERSION_MASK) != VERSION)
throw new IllegalArgumentException("invalid version: " +
(((one & VERSION_MASK) >> 3) & 0x1F));
if((two & RESERVED_MASK) != RESERVED)
throw new IllegalArgumentException("invalid reserved: " +
(two & RESERVED_MASK));
}
/**
* Validates the given DIMERecord, throwing IllegalArgumentException
* if any fields are invalid.
*/
private void validate() {
validateFirstBytes(_byte1, _byte2);
byte maskedType = (byte)(_byte2 & TYPE_MASK);
switch(maskedType) {
case TYPE_UNCHANGED:
if( getTypeLength() != 0)
throw new IllegalArgumentException(
"TYPE_UNCHANGED requires 0 type length");
break;
case TYPE_MEDIA_TYPE:
break;
case TYPE_ABSOLUTE_URI:
break;
case TYPE_UNKNOWN:
if( getTypeLength() != 0)
throw new IllegalArgumentException(
"TYPE_UNKNOWN requires 0 type length");
break;
case TYPE_NONE:
if( getTypeLength() != 0 || getDataLength() != 0)
throw new IllegalArgumentException(
"TYPE_NONE requires 0 type & data length");
break;
default:
throw new IllegalArgumentException(
"invalid type: " + ((maskedType >> 4) & 0x0F));
}
}
/**
* Reads data from the input stream, skipping padded bytes if necessary.
*/
private static byte[] readInformation(int length, InputStream in)
throws IOException {
if(length == 0)
return DataUtils.EMPTY_BYTE_ARRAY;
byte[] info = new byte[length];
fillBuffer(info, in);
skipPaddedData(length, in);
return info;
}
/**
* Writes the given data to an output stream, including padding.
*/
private static void writeDataWithPadding(byte[] data, OutputStream os)
throws IOException {
if(data.length == 0)
return;
os.write(data);
writePadding(data.length, os);
}
/**
* Calculates how much data should be padded for the given length.
*/
private static int calculatePaddingLength(int length) {
return (length % 4 == 0) ? 0 : (4 - length % 4);
}
/**
* Skips however much data was padded for the given length.
*/
private static void skipPaddedData(int length, InputStream in)
throws IOException {
int padding = calculatePaddingLength(length);
long skipped = 0;
while(skipped < padding) {
long current = in.skip(padding - skipped);
if(current == -1 || current == 0)
throw new IOException("eof");
else
skipped += current;
}
}
/**
* Fills up the byte array with data from the stream.
*/
private static void fillBuffer(byte[] buffer, InputStream in)
throws IOException {
int offset = 0;
while (offset < buffer.length) {
int read = in.read(buffer, offset, buffer.length - offset);
if(read == -1)
throw new IOException("eof");
else
offset += read;
}
}
/**
* Parses a byte array of options into a Map.
*/
private static Map parseOptions(byte[] options)
throws DIMEMessageException {
Map map = new HashMap();
int offset = 0;
while (offset < options.length) {
if (options.length - offset < 4)
throw new DIMEMessageException("illegal options field");
byte[] keyBytes = new byte[2];
System.arraycopy(options, offset, keyBytes, 0, 2);
String key;
try {
key = new String(keyBytes, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// simply ignore this option
key = null;
}
offset += 2;
int valueLength = ByteOrder.beb2int(options, offset, 2);
offset += 2;
if (options.length - offset < valueLength)
throw new DIMEMessageException("illegal options field");
byte[] valueBytes = new byte[valueLength];
System.arraycopy(options, offset, valueBytes, 0, valueLength);
String value;
try {
value = new String(valueBytes, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// simply ignore this option
value = null;
}
offset += valueLength;
if (key != null && value != null)
map.put(key, value);
}
return map;
}
}