package com.gvaneyck.rtmp.encoding;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Decodes AMF3 data and packets from RTMP
*
* @author Gabriel Van Eyck
*/
public class AMF3Decoder {
/** Stores the data to be consumed while decoding */
private byte[] dataBuffer;
private int dataPos;
/** Lists of references and class definitions seen so far */
private List<String> stringReferences = new ArrayList<String>();
private List<Object> objectReferences = new ArrayList<Object>();
private List<ClassDefinition> classDefinitions = new ArrayList<ClassDefinition>();
/**
* Resets all the reference lists
*/
public void reset() {
stringReferences.clear();
objectReferences.clear();
classDefinitions.clear();
}
/**
* Decodes the result of a connect call
*
* @param data The connect result
* @return The decoded object
* @throws EncodingException
* @throws NotImplementedException
*/
public TypedObject decodeConnect(byte[] data) throws NotImplementedException, EncodingException {
reset();
dataBuffer = data;
dataPos = 0;
TypedObject result = new TypedObject("Invoke");
result.put("result", decodeAMF0());
result.put("invokeId", decodeAMF0());
result.put("serviceCall", decodeAMF0());
result.put("data", decodeAMF0());
if (dataPos != dataBuffer.length)
throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length);
return result;
}
/**
* Decodes the result of a invoke call
*
* @param data The invoke result
* @return The decoded object
* @throws EncodingException
* @throws NotImplementedException
*/
public TypedObject decodeInvoke(byte[] data) throws NotImplementedException, EncodingException {
reset();
dataBuffer = data;
dataPos = 0;
TypedObject result = new TypedObject("Invoke");
if (dataBuffer[0] == 0x00) {
dataPos++;
result.put("version", 0x00);
}
result.put("result", decodeAMF0());
result.put("invokeId", decodeAMF0());
result.put("serviceCall", decodeAMF0());
result.put("data", decodeAMF0());
if (dataPos != dataBuffer.length)
throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length);
return result;
}
/**
* Decodes data according to AMF3
*
* @param data The data to decode
* @return The decoded object
* @throws NotImplementedException
* @throws EncodingException
*/
public Object decode(byte[] data) throws EncodingException, NotImplementedException {
dataBuffer = data;
dataPos = 0;
Object result = decode();
if (dataPos != dataBuffer.length)
throw new EncodingException("Did not consume entire buffer: " + dataPos + " of " + dataBuffer.length);
return result;
}
/**
* Decodes AMF3 data in the buffer
*
* @return The decoded object
* @throws EncodingException
* @throws NotImplementedException
*/
private Object decode() throws EncodingException, NotImplementedException {
byte type = readByte();
switch (type) {
case 0x00:
throw new EncodingException("Undefined data type");
case 0x01:
return null;
case 0x02:
return false;
case 0x03:
return true;
case 0x04:
return readInt();
case 0x05:
return readDouble();
case 0x06:
return readString();
case 0x07:
return readXML();
case 0x08:
return readDate();
case 0x09:
return readArray();
case 0x0A:
return readObject();
case 0x0B:
return readXMLString();
case 0x0C:
return readByteArray();
}
throw new EncodingException("Unexpected AMF3 data type: " + type);
}
/**
* Removes a single byte from the data buffer
*
* @return The next byte in the data buffer
*/
private byte readByte() {
byte ret = dataBuffer[dataPos];
dataPos++;
return ret;
}
/**
* Removes a single byte from the data buffer as an unsigned integer
*
* @return The next byte in the data buffer as an unsigned integer
*/
private int readByteAsInt() {
int ret = readByte();
if (ret < 0)
ret += 256;
return ret;
}
/**
* Removes the next 'length' bytes from the data buffer
*
* @param length The number of bytes to retrieve
* @return The next 'length' bytes in the data buffer
*/
private byte[] readBytes(int length) {
byte[] ret = new byte[length];
for (int i = 0; i < length; i++) {
ret[i] = dataBuffer[dataPos];
dataPos++;
}
return ret;
}
/**
* Decodes an AMF3 integer
*
* @return The decoded integer
* @author FluorineFX
*/
private int readInt() {
int ret = readByteAsInt();
int tmp;
if (ret < 128) {
return ret;
}
else {
ret = (ret & 0x7f) << 7;
tmp = readByteAsInt();
if (tmp < 128) {
ret = ret | tmp;
}
else {
ret = (ret | tmp & 0x7f) << 7;
tmp = readByteAsInt();
if (tmp < 128) {
ret = ret | tmp;
}
else {
ret = (ret | tmp & 0x7f) << 8;
tmp = readByteAsInt();
ret = ret | tmp;
}
}
}
// Sign extend
int mask = 1 << 28;
int r = -(ret & mask) | ret;
return r;
}
/**
* Decodes an AMF3 double
*
* @return The decoded double
*/
private double readDouble() {
long value = 0;
for (int i = 0; i < 8; i++)
value = (value << 8) + readByteAsInt();
return Double.longBitsToDouble(value);
}
/**
* Decodes an AMF3 string
*
* @return The decoded string
* @throws EncodingException
*/
private String readString() throws EncodingException {
int handle = readInt();
boolean inline = ((handle & 1) != 0);
handle = handle >> 1;
if (inline) {
if (handle == 0)
return "";
byte[] data = readBytes(handle);
String str;
try {
str = new String(data, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new EncodingException("Error parsing AMF3 string from " + data);
}
stringReferences.add(str);
return str;
}
else {
return stringReferences.get(handle);
}
}
/**
* Not implemented
*
* @return
* @throws NotImplementedException
*/
private String readXML() throws NotImplementedException {
throw new NotImplementedException("Reading of XML is not implemented");
}
/**
* Decodes an AMF3 date
*
* @return The decoded date
*/
private Date readDate() {
int handle = readInt();
boolean inline = ((handle & 1) != 0);
handle = handle >> 1;
if (inline) {
long ms = (long)readDouble();
Date d = new Date(ms);
objectReferences.add(d);
return d;
}
else {
return (Date)objectReferences.get(handle);
}
}
/**
* Decodes an AMF3 (non-associative) array
*
* @return The decoded array
* @throws EncodingException
* @throws NotImplementedException
*/
private Object[] readArray() throws EncodingException, NotImplementedException {
int handle = readInt();
boolean inline = ((handle & 1) != 0);
handle = handle >> 1;
if (inline) {
String key = readString();
if (key != null && !key.equals(""))
throw new NotImplementedException("Associative arrays are not supported");
Object[] ret = new Object[handle];
objectReferences.add(ret);
for (int i = 0; i < handle; i++)
ret[i] = decode();
return ret;
}
else {
return (Object[])objectReferences.get(handle);
}
}
/**
* Decodes an AMF3 object
*
* @return The decoded object
* @throws EncodingException
* @throws NotImplementedException
*/
private Object readObject() throws EncodingException, NotImplementedException {
int handle = readInt();
boolean inline = ((handle & 1) != 0);
handle = handle >> 1;
if (inline) {
boolean inlineDefine = ((handle & 1) != 0);
handle = handle >> 1;
ClassDefinition cd;
if (inlineDefine) {
cd = new ClassDefinition();
cd.type = readString();
cd.externalizable = ((handle & 1) != 0);
handle = handle >> 1;
cd.dynamic = ((handle & 1) != 0);
handle = handle >> 1;
for (int i = 0; i < handle; i++)
cd.members.add(readString());
classDefinitions.add(cd);
}
else {
cd = classDefinitions.get(handle);
}
TypedObject ret = new TypedObject(cd.type);
// Need to add reference here due to circular references
objectReferences.add(ret);
if (cd.externalizable) {
if (cd.type.equals("DSK"))
ret = readDSK();
else if (cd.type.equals("DSA"))
ret = readDSA();
else if (cd.type.equals("flex.messaging.io.ArrayCollection")) {
Object obj = decode();
ret = TypedObject.makeArrayCollection((Object[])obj);
}
else if (cd.type.equals("com.riotgames.platform.systemstate.ClientSystemStatesNotification") || cd.type.equals("com.riotgames.platform.broadcast.BroadcastNotification")) {
int size = 0;
for (int i = 0; i < 4; i++)
size = size * 256 + readByteAsInt();
String json;
try {
json = new String(readBytes(size), "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new EncodingException(e.toString());
}
ret = new TypedObject((ObjectMap)JSON.parse(json));
ret.type = cd.type;
}
else {
for (int i = dataPos; i < dataBuffer.length; i++)
System.out.print(String.format("%02X", dataBuffer[i]));
System.out.println();
throw new NotImplementedException("Externalizable not handled for " + cd.type);
}
}
else {
for (int i = 0; i < cd.members.size(); i++) {
String key = cd.members.get(i);
Object value = decode();
ret.put(key, value);
}
if (cd.dynamic) {
String key;
while ((key = readString()).length() != 0) {
Object value = decode();
ret.put(key, value);
}
}
}
return ret;
}
else {
return objectReferences.get(handle);
}
}
/**
* Not implemented
*
* @return
* @throws NotImplementedException
*/
private String readXMLString() throws NotImplementedException {
throw new NotImplementedException("Reading of XML strings is not implemented");
}
/**
* Decodes an AMF3 byte array
*
* @return The decoded byte array
*/
private byte[] readByteArray() {
int handle = readInt();
boolean inline = ((handle & 1) != 0);
handle = handle >> 1;
if (inline) {
byte[] ret = readBytes(handle);
objectReferences.add(ret);
return ret;
}
else {
return (byte[])objectReferences.get(handle);
}
}
/**
* Decodes a DSA
*
* @return The decoded DSA
* @throws NotImplementedException
* @throws EncodingException
*/
private TypedObject readDSA() throws EncodingException, NotImplementedException {
TypedObject ret = new TypedObject("DSA");
int flag;
List<Integer> flags = readFlags();
for (int i = 0; i < flags.size(); i++) {
flag = flags.get(i);
int bits = 0;
if (i == 0) {
if ((flag & 0x01) != 0)
ret.put("body", decode());
if ((flag & 0x02) != 0)
ret.put("clientId", decode());
if ((flag & 0x04) != 0)
ret.put("destination", decode());
if ((flag & 0x08) != 0)
ret.put("headers", decode());
if ((flag & 0x10) != 0)
ret.put("messageId", decode());
if ((flag & 0x20) != 0)
ret.put("timeStamp", decode());
if ((flag & 0x40) != 0)
ret.put("timeToLive", decode());
bits = 7;
}
else if (i == 1) {
if ((flag & 0x01) != 0) {
readByte();
byte[] temp = readByteArray();
ret.put("clientIdBytes", temp);
ret.put("clientId", byteArrayToID(temp));
}
if ((flag & 0x02) != 0) {
readByte();
byte[] temp = readByteArray();
ret.put("messageIdBytes", temp);
ret.put("messageId", byteArrayToID(temp));
}
bits = 2;
}
readRemaining(flag, bits);
}
flags = readFlags();
for (int i = 0; i < flags.size(); i++) {
flag = flags.get(i);
int bits = 0;
if (i == 0) {
if ((flag & 0x01) != 0)
ret.put("correlationId", decode());
if ((flag & 0x02) != 0) {
readByte();
byte[] temp = readByteArray();
ret.put("correlationIdBytes", temp);
ret.put("correlationId", byteArrayToID(temp));
}
bits = 2;
}
readRemaining(flag, bits);
}
return ret;
}
/**
* Decodes a DSK
*
* @return The decoded DSK
* @throws NotImplementedException
* @throws EncodingException
*/
private TypedObject readDSK() throws EncodingException, NotImplementedException {
// DSK is just a DSA + extra set of flags/objects
TypedObject ret = readDSA();
ret.type = "DSK";
List<Integer> flags = readFlags();
for (int i = 0; i < flags.size(); i++)
readRemaining(flags.get(i), 0);
return ret;
}
private List<Integer> readFlags() {
List<Integer> flags = new ArrayList<Integer>();
int flag;
do {
flag = readByteAsInt();
flags.add(flag);
} while ((flag & 0x80) != 0);
return flags;
}
private void readRemaining(int flag, int bits) throws EncodingException, NotImplementedException {
// For forwards compatibility, read in any other flagged objects to
// preserve the integrity of the input stream...
if ((flag >> bits) != 0) {
for (int o = bits; o < 6; o++) {
if (((flag >> o) & 1) != 0)
decode();
}
}
}
/**
* Converts an array of bytes into an ID string
*
* @return The ID string
*/
private String byteArrayToID(byte[] data) {
StringBuilder ret = new StringBuilder();
for (int i = 0; i < data.length; i++) {
if (i == 4 || i == 6 || i == 8 || i == 10)
ret.append('-');
ret.append(String.format("%02x", data[i]));
}
return ret.toString();
}
/**
* Decodes the next AMF0 object from the buffer
*
* @return The decoded object
* @throws NotImplementedException
* @throws EncodingException
*/
private Object decodeAMF0() throws NotImplementedException, EncodingException {
int type = readByte();
switch (type) {
case 0x00:
return readIntAMF0();
case 0x02:
return readStringAMF0();
case 0x03:
return readObjectAMF0();
case 0x05:
return null;
case 0x11: // AMF3
return decode();
}
throw new NotImplementedException("AMF0 type not supported: " + type);
}
/**
* Decodes an AMF0 string
*
* @return The decoded string
* @throws EncodingException
*/
private String readStringAMF0() throws EncodingException {
int length = (readByteAsInt() << 8) + readByteAsInt();
if (length == 0)
return "";
byte[] data = readBytes(length);
// UTF-8 applicable?
String str;
try {
str = new String(data, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new EncodingException("Error parsing AMF0 string from " + data);
}
return str;
}
/**
* Decodes an AMF0 integer
*
* @return The decoded integer
*/
private int readIntAMF0() {
return (int)readDouble();
}
/**
* Decodes an AMF0 object
*
* @return The decoded object
* @throws EncodingException
* @throws NotImplementedException
*/
private TypedObject readObjectAMF0() throws EncodingException, NotImplementedException {
TypedObject body = new TypedObject("Body");
String key;
while (!(key = readStringAMF0()).equals("")) {
byte b = readByte();
if (b == 0x00)
body.put(key, readDouble());
else if (b == 0x02)
body.put(key, readStringAMF0());
else if (b == 0x05)
body.put(key, null);
else
throw new NotImplementedException("AMF0 type not supported: " + b);
}
readByte(); // Skip object end marker
return body;
}
}