Fixed issue 4, bad calculation of remaining length in big PublishDecoder.

This commit is contained in:
andrea
2012-09-16 17:22:13 +02:00
parent 522a0ecb4a
commit ecea97f409
6 changed files with 773 additions and 659 deletions
@@ -1,57 +1,63 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession; import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoderResult; import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage; import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType; import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType;
import org.dna.mqtt.moquette.proto.messages.PublishMessage; import org.dna.mqtt.moquette.proto.messages.PublishMessage;
import org.slf4j.Logger;
/** import org.slf4j.LoggerFactory;
*
* @author andrea /**
*/ *
public class PublishDecoder extends MqttDecoder { * @author andrea
*/
public MessageDecoderResult decodable(IoSession session, IoBuffer in) { public class PublishDecoder extends MqttDecoder {
return Utils.checkDecodable(AbstractMessage.PUBLISH, in);
} private static Logger LOG = LoggerFactory.getLogger(PublishDecoder.class);
public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
int startPos = in.position(); return Utils.checkDecodable(AbstractMessage.PUBLISH, in);
}
//Common decoding part
PublishMessage message = new PublishMessage(); public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
if (decodeCommonHeader(message, in) == NEED_DATA) { LOG.debug("decode invoked with buffer " + in);
return NEED_DATA; int startPos = in.position();
}
int remainingLength = message.getRemainingLength(); //Common decoding part
PublishMessage message = new PublishMessage();
//Topic name if (decodeCommonHeader(message, in) == NEED_DATA) {
String topic = Utils.decodeString(in); LOG.info("decode ask for more data after " + in);
if (topic == null) { return NEED_DATA;
return NEED_DATA; }
} int remainingLength = message.getRemainingLength();
message.setTopicName(topic);
//Topic name
if (message.getQos() == QOSType.LEAST_ONE || String topic = Utils.decodeString(in);
message.getQos() == QOSType.EXACTLY_ONCE) { if (topic == null) {
message.setMessageID(Utils.readWord(in)); return NEED_DATA;
} }
int stopPos = in.position(); message.setTopicName(topic);
//read the payload if (message.getQos() == QOSType.LEAST_ONE ||
int payloadSize = remainingLength - (stopPos - startPos - 2); message.getQos() == QOSType.EXACTLY_ONCE) {
byte[] b = new byte[payloadSize]; message.setMessageID(Utils.readWord(in));
if (in.remaining() < payloadSize) { }
return NEED_DATA; int stopPos = in.position();
}
in.get(b); //read the payload
message.setPayload(b); int payloadSize = remainingLength - (stopPos - startPos - 2) + (Utils.numBytesToEncode(remainingLength) - 1);
if (in.remaining() < payloadSize) {
out.write(message); return NEED_DATA;
return OK; }
} byte[] b = new byte[payloadSize];
in.get(b);
} message.setPayload(b);
out.write(message);
return OK;
}
}
@@ -1,48 +1,48 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession; import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.demux.MessageEncoder; import org.apache.mina.filter.codec.demux.MessageEncoder;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage; import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType; import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType;
import org.dna.mqtt.moquette.proto.messages.PublishMessage; import org.dna.mqtt.moquette.proto.messages.PublishMessage;
/** /**
* *
* @author andrea * @author andrea
*/ */
public class PublishEncoder implements MessageEncoder<PublishMessage> { public class PublishEncoder implements MessageEncoder<PublishMessage> {
public void encode(IoSession session, PublishMessage message, ProtocolEncoderOutput out) throws Exception { public void encode(IoSession session, PublishMessage message, ProtocolEncoderOutput out) throws Exception {
if (message.getQos() == QOSType.RESERVED) { if (message.getQos() == QOSType.RESERVED) {
throw new IllegalArgumentException("Found a message with RESERVED Qos"); throw new IllegalArgumentException("Found a message with RESERVED Qos");
} }
if (message.getTopicName() == null || message.getTopicName().isEmpty()) { if (message.getTopicName() == null || message.getTopicName().isEmpty()) {
throw new IllegalArgumentException("Found a message with empty or null topic name"); throw new IllegalArgumentException("Found a message with empty or null topic name");
} }
IoBuffer variableHeaderBuff = IoBuffer.allocate(2).setAutoExpand(true); IoBuffer variableHeaderBuff = IoBuffer.allocate(2).setAutoExpand(true);
variableHeaderBuff.put(Utils.encodeString(message.getTopicName())); variableHeaderBuff.put(Utils.encodeString(message.getTopicName()));
if (message.getQos() == QOSType.LEAST_ONE || if (message.getQos() == QOSType.LEAST_ONE ||
message.getQos() == QOSType.EXACTLY_ONCE ) { message.getQos() == QOSType.EXACTLY_ONCE ) {
if (message.getMessageID() == null) { if (message.getMessageID() == null) {
throw new IllegalArgumentException("Found a message with QOS 1 or 2 and not MessageID setted"); throw new IllegalArgumentException("Found a message with QOS 1 or 2 and not MessageID setted");
} }
Utils.writeWord(variableHeaderBuff, message.getMessageID()); Utils.writeWord(variableHeaderBuff, message.getMessageID());
} }
variableHeaderBuff.put(message.getPayload()); variableHeaderBuff.put(message.getPayload());
variableHeaderBuff.flip(); variableHeaderBuff.flip();
int variableHeaderSize = variableHeaderBuff.remaining(); int variableHeaderSize = variableHeaderBuff.remaining();
byte flags = Utils.encodeFlags(message); byte flags = Utils.encodeFlags(message);
IoBuffer buff = IoBuffer.allocate(2 + variableHeaderSize); IoBuffer buff = IoBuffer.allocate(2 + variableHeaderSize).setAutoExpand(true);;
buff.put((byte) (AbstractMessage.PUBLISH << 4 | flags)); buff.put((byte) (AbstractMessage.PUBLISH << 4 | flags));
buff.put(Utils.encodeRemainingLength(variableHeaderSize)); buff.put(Utils.encodeRemainingLength(variableHeaderSize));
buff.put(variableHeaderBuff).flip(); buff.put(variableHeaderBuff).flip();
out.write(buff); out.write(buff);
} }
} }
@@ -1,180 +1,191 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.filter.codec.demux.MessageDecoderResult; import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage; import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Common utils methodd used in codecs. * Common utils methodd used in codecs.
* *
* @author andrea * @author andrea
*/ */
public class Utils { public class Utils {
public static final int MAX_LENGTH_LIMIT = 268435455; public static final int MAX_LENGTH_LIMIT = 268435455;
/** /**
* Read 2 bytes from in buffer first MSB, and then LSB returning as int. * Read 2 bytes from in buffer first MSB, and then LSB returning as int.
*/ */
static int readWord(IoBuffer in) { static int readWord(IoBuffer in) {
int msb = in.get() & 0x00FF; //remove sign extension due to casting int msb = in.get() & 0x00FF; //remove sign extension due to casting
int lsb = in.get() & 0x00FF; int lsb = in.get() & 0x00FF;
msb = (msb << 8) | lsb ; msb = (msb << 8) | lsb ;
return msb; return msb;
} }
/** /**
* Writes as 2 bytes the int value into buffer first MSB, and then LSB. * Writes as 2 bytes the int value into buffer first MSB, and then LSB.
*/ */
static void writeWord(IoBuffer out, int value) { static void writeWord(IoBuffer out, int value) {
out.put((byte) ((value & 0xFF00) >> 8)); //msb out.put((byte) ((value & 0xFF00) >> 8)); //msb
out.put((byte) (value & 0x00FF)); //lsb out.put((byte) (value & 0x00FF)); //lsb
} }
/** /**
* Decode the variable remaining lenght as defined in MQTT v3.1 specification * Decode the variable remaining lenght as defined in MQTT v3.1 specification
* (section 2.1). * (section 2.1).
* *
* @return the decoded length or -1 if needed more data to decode the length field. * @return the decoded length or -1 if needed more data to decode the length field.
*/ */
static int decodeRemainingLenght(IoBuffer in) { static int decodeRemainingLenght(IoBuffer in) {
int multiplier = 1; int multiplier = 1;
int value = 0; int value = 0;
byte digit; byte digit;
do { do {
if (in.remaining() < 1) { if (in.remaining() < 1) {
return -1; return -1;
} }
digit = in.get(); digit = in.get();
value += (digit & 0x7F) * multiplier; value += (digit & 0x7F) * multiplier;
multiplier *= 128; multiplier *= 128;
} while ((digit & 0x80) != 0); } while ((digit & 0x80) != 0);
return value; return value;
} }
/** /**
* Encode the value in the format defined in specification as variable length * Return the number of bytes to encode the gicen remaining length value
* array. */
* static int numBytesToEncode(int len) {
* @throws IllegalArgumentException if the value is not in the specification bounds if (0 <= len && len <= 127) return 1;
* [0..268435455]. if (128 <= len && len <= 16383) return 2;
*/ if (16384 <= len && len <= 2097151) return 3;
static IoBuffer encodeRemainingLength(int value) throws IllegalAccessException { if (2097152 <= len && len <= 268435455) return 4;
if (value > MAX_LENGTH_LIMIT || value < 0) { throw new IllegalArgumentException("value shoul be in the range [0..268435455]");
throw new IllegalAccessException("Value should in range 0.." + MAX_LENGTH_LIMIT + " found " + value); }
}
/**
IoBuffer encoded = IoBuffer.allocate(4); * Encode the value in the format defined in specification as variable length
byte digit; * array.
do { *
digit = (byte) (value % 128); * @throws IllegalArgumentException if the value is not in the specification bounds
value = value / 128; * [0..268435455].
// if there are more digits to encode, set the top bit of this digit */
if (value > 0) { static IoBuffer encodeRemainingLength(int value) throws IllegalAccessException {
digit = (byte) (digit | 0x80); if (value > MAX_LENGTH_LIMIT || value < 0) {
} throw new IllegalAccessException("Value should in range 0.." + MAX_LENGTH_LIMIT + " found " + value);
encoded.put(digit); }
} while (value > 0);
encoded.flip(); IoBuffer encoded = IoBuffer.allocate(4);
return encoded; byte digit;
} do {
digit = (byte) (value % 128);
static MessageDecoderResult checkDecodable(byte type, IoBuffer in) { value = value / 128;
if (in.remaining() < 1) { // if there are more digits to encode, set the top bit of this digit
return MessageDecoderResult.NEED_DATA; if (value > 0) {
} digit = (byte) (digit | 0x80);
byte h1 = in.get(); }
byte messageType = (byte) ((h1 & 0x00F0) >> 4); encoded.put(digit);
} while (value > 0);
int remainingLength = Utils.decodeRemainingLenght(in); encoded.flip();
if (remainingLength == -1) { return encoded;
return MessageDecoderResult.NEED_DATA; }
}
static MessageDecoderResult checkDecodable(byte type, IoBuffer in) {
//check remaining length if (in.remaining() < 1) {
if (in.remaining() < remainingLength) { return MessageDecoderResult.NEED_DATA;
return MessageDecoderResult.NEED_DATA; }
} byte h1 = in.get();
byte messageType = (byte) ((h1 & 0x00F0) >> 4);
return messageType == type ? MessageDecoderResult.OK : MessageDecoderResult.NOT_OK;
} int remainingLength = Utils.decodeRemainingLenght(in);
if (remainingLength == -1) {
/** return MessageDecoderResult.NEED_DATA;
* Return the IoBuffer with string encoded as MSB, LSB and UTF-8 encoded }
* string content.
*/ //check remaining length
static IoBuffer encodeString(String str) { if (in.remaining() < remainingLength) {
IoBuffer out = IoBuffer.allocate(2).setAutoExpand(true); return MessageDecoderResult.NEED_DATA;
byte[] raw; }
try {
raw = str.getBytes("UTF-8"); return messageType == type ? MessageDecoderResult.OK : MessageDecoderResult.NOT_OK;
//NB every Java platform has got UTF-8 encoding by default, so this }
//exception are never raised.
} catch (UnsupportedEncodingException ex) { /**
LoggerFactory.getLogger(ConnectEncoder.class).error(null, ex); * Return the IoBuffer with string encoded as MSB, LSB and UTF-8 encoded
return null; * string content.
} */
Utils.writeWord(out, raw.length); static IoBuffer encodeString(String str) {
out.put(raw).flip(); IoBuffer out = IoBuffer.allocate(2).setAutoExpand(true);
return out; byte[] raw;
} try {
raw = str.getBytes("UTF-8");
//NB every Java platform has got UTF-8 encoding by default, so this
/** //exception are never raised.
* Load a string from the given buffer, reading first the two bytes of len } catch (UnsupportedEncodingException ex) {
* and then the UTF-8 bytes of the string. LoggerFactory.getLogger(ConnectEncoder.class).error(null, ex);
* return null;
* @return the decoded string or null if NEED_DATA }
*/ Utils.writeWord(out, raw.length);
static String decodeString(IoBuffer in) throws UnsupportedEncodingException { out.put(raw).flip();
if (in.remaining() < 2) { return out;
return null; }
}
int strLen = Utils.readWord(in);
if (in.remaining() < strLen) { /**
return null; * Load a string from the given buffer, reading first the two bytes of len
} * and then the UTF-8 bytes of the string.
byte[] strRaw = new byte[strLen]; *
in.get(strRaw); * @return the decoded string or null if NEED_DATA
*/
return new String(strRaw, "UTF-8"); static String decodeString(IoBuffer in) throws UnsupportedEncodingException {
} if (in.remaining() < 2) {
return null;
static byte encodeFlags(AbstractMessage message) { }
byte flags = 0; int strLen = Utils.readWord(in);
if (message.isDupFlag()) { if (in.remaining() < strLen) {
flags |= 0x08; return null;
} }
if (message.isRetainFlag()) { byte[] strRaw = new byte[strLen];
flags |= 0x01; in.get(strRaw);
}
return new String(strRaw, "UTF-8");
flags |= ((message.getQos().ordinal() & 0x03) << 1); }
return flags;
} static byte encodeFlags(AbstractMessage message) {
byte flags = 0;
/** if (message.isDupFlag()) {
* Converts MQTT message type to a textual description. flags |= 0x08;
* */ }
public static String msgType2String(int type) { if (message.isRetainFlag()) {
switch (type) { flags |= 0x01;
case AbstractMessage.CONNECT: return "CONNECT"; }
case AbstractMessage.CONNACK: return "CONNACK";
case AbstractMessage.PUBLISH: return "PUBLISH"; flags |= ((message.getQos().ordinal() & 0x03) << 1);
case AbstractMessage.PUBACK: return "PUBACK"; return flags;
case AbstractMessage.PUBREC: return "PUBREC"; }
case AbstractMessage.PUBREL: return "PUBREL";
case AbstractMessage.PUBCOMP: return "PUBCOMP"; /**
case AbstractMessage.SUBSCRIBE: return "SUBSCRIBE"; * Converts MQTT message type to a textual description.
case AbstractMessage.SUBACK: return "SUBACK"; * */
case AbstractMessage.UNSUBSCRIBE: return "UNSUBSCRIBE"; public static String msgType2String(int type) {
case AbstractMessage.UNSUBACK: return "UNSUBACK"; switch (type) {
case AbstractMessage.PINGREQ: return "PINGREQ"; case AbstractMessage.CONNECT: return "CONNECT";
case AbstractMessage.PINGRESP: return "PINGRESP"; case AbstractMessage.CONNACK: return "CONNACK";
case AbstractMessage.DISCONNECT: return "DISCONNECT"; case AbstractMessage.PUBLISH: return "PUBLISH";
default: throw new RuntimeException("Can't decode message type " + type); case AbstractMessage.PUBACK: return "PUBACK";
} case AbstractMessage.PUBREC: return "PUBREC";
} case AbstractMessage.PUBREL: return "PUBREL";
} case AbstractMessage.PUBCOMP: return "PUBCOMP";
case AbstractMessage.SUBSCRIBE: return "SUBSCRIBE";
case AbstractMessage.SUBACK: return "SUBACK";
case AbstractMessage.UNSUBSCRIBE: return "UNSUBSCRIBE";
case AbstractMessage.UNSUBACK: return "UNSUBACK";
case AbstractMessage.PINGREQ: return "PINGREQ";
case AbstractMessage.PINGRESP: return "PINGRESP";
case AbstractMessage.DISCONNECT: return "DISCONNECT";
default: throw new RuntimeException("Can't decode message type " + type);
}
}
}
@@ -1,132 +1,180 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import org.apache.mina.core.buffer.IoBuffer; import java.io.File;
import org.apache.mina.filter.codec.demux.MessageDecoder; import java.io.FileInputStream;
import org.apache.mina.filter.codec.demux.MessageDecoderResult; import java.io.FileNotFoundException;
import org.dna.mqtt.moquette.proto.TestUtils.MockProtocolDecoderOutput; import java.io.IOException;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage; import org.apache.mina.core.buffer.IoBuffer;
import org.dna.mqtt.moquette.proto.messages.PublishMessage; import org.apache.mina.filter.codec.demux.MessageDecoder;
import static org.junit.Assert.*; import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import org.junit.Before; import org.dna.mqtt.moquette.proto.TestUtils.MockProtocolDecoderOutput;
import org.junit.Test; import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
import org.dna.mqtt.moquette.proto.messages.PublishMessage;
/** import static org.junit.Assert.*;
* import org.junit.Before;
* @author andrea import org.junit.Ignore;
*/ import org.junit.Test;
public class PublishDecoderTest {
/**
IoBuffer m_buff; *
PublishDecoder m_msgdec; * @author andrea
MockProtocolDecoderOutput<PublishMessage> m_mockProtoDecoder; */
public class PublishDecoderTest {
@Before
public void setUp() { IoBuffer m_buff;
m_buff = IoBuffer.allocate(4); PublishDecoder m_msgdec;
m_msgdec = new PublishDecoder(); MockProtocolDecoderOutput<PublishMessage> m_mockProtoDecoder;
m_mockProtoDecoder = new MockProtocolDecoderOutput<PublishMessage>();
} @Before
public void setUp() {
@Test m_buff = IoBuffer.allocate(4);
public void testDecodable_OK() { m_msgdec = new PublishDecoder();
m_buff.put((byte) (AbstractMessage.PUBLISH << 4)) m_mockProtoDecoder = new MockProtocolDecoderOutput<PublishMessage>();
.put((byte)0) //0 length }
.flip();
assertEquals(MessageDecoder.OK, m_msgdec.decodable(null, m_buff)); @Test
} public void testDecodable_OK() {
m_buff.put((byte) (AbstractMessage.PUBLISH << 4))
@Test .put((byte) 0) //0 length
public void testHeader() throws Exception { .flip();
m_buff = IoBuffer.allocate(14); assertEquals(MessageDecoder.OK, m_msgdec.decodable(null, m_buff));
initHeader(m_buff); }
m_buff.flip();
@Test
//Exercise public void testHeader() throws Exception {
MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder); m_buff = IoBuffer.allocate(14);
initHeader(m_buff);
assertNotNull(m_mockProtoDecoder.getMessage()); m_buff.flip();
assertEquals(MessageDecoder.OK, res);
assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName()); //Exercise
assertNull(m_mockProtoDecoder.getMessage().getMessageID()); MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder);
assertEquals(AbstractMessage.PUBLISH, m_mockProtoDecoder.getMessage().getMessageType());
} assertNotNull(m_mockProtoDecoder.getMessage());
assertEquals(MessageDecoder.OK, res);
@Test assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName());
public void testHeaderWithMessageID() throws Exception { assertNull(m_mockProtoDecoder.getMessage().getMessageID());
m_buff = IoBuffer.allocate(14).setAutoExpand(true); assertEquals(AbstractMessage.PUBLISH, m_mockProtoDecoder.getMessage().getMessageType());
int messageID = 123; }
initHeaderWithMessageID(m_buff, messageID);
m_buff.flip(); @Test
public void testHeaderWithMessageID() throws Exception {
//Exercise m_buff = IoBuffer.allocate(14).setAutoExpand(true);
MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder); int messageID = 123;
initHeaderWithMessageID(m_buff, messageID);
assertNotNull(m_mockProtoDecoder.getMessage()); m_buff.flip();
assertEquals(MessageDecoder.OK, res);
assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName()); //Exercise
assertEquals(messageID, (int) m_mockProtoDecoder.getMessage().getMessageID()); MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder);
}
assertNotNull(m_mockProtoDecoder.getMessage());
@Test assertEquals(MessageDecoder.OK, res);
public void testHeaderWithMessageID_Payload() throws Exception { assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName());
m_buff = IoBuffer.allocate(14).setAutoExpand(true); assertEquals(messageID, (int) m_mockProtoDecoder.getMessage().getMessageID());
int messageID = 123; }
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
initHeaderWithMessageID_Payload(m_buff, messageID, payload); @Test
m_buff.flip(); public void testHeaderWithMessageID_Payload() throws Exception {
m_buff = IoBuffer.allocate(14).setAutoExpand(true);
//Exercise int messageID = 123;
MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder); byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
initHeaderWithMessageID_Payload(m_buff, messageID, payload);
assertNotNull(m_mockProtoDecoder.getMessage()); m_buff.flip();
assertEquals(MessageDecoder.OK, res);
assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName()); //Exercise
assertEquals(messageID, (int) m_mockProtoDecoder.getMessage().getMessageID()); MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder);
TestUtils.verifyEquals(payload, m_mockProtoDecoder.getMessage().getPayload());
} assertNotNull(m_mockProtoDecoder.getMessage());
assertEquals(MessageDecoder.OK, res);
@Test assertEquals("Fake Topic", m_mockProtoDecoder.getMessage().getTopicName());
public void testBugOnRealCase() throws Exception { assertEquals(messageID, (int) m_mockProtoDecoder.getMessage().getMessageID());
byte[] overallMessage = new byte[] {0x30, 0x17, //fixed header, 25 byte lenght TestUtils.verifyEquals(payload, m_mockProtoDecoder.getMessage().getPayload());
0x00, 0x06, 0x2f, 0x74, 0x6f, 0x70, 0x69, 0x63, //[/topic] string 2 len + 6 content }
0x54, 0x65, 0x73, 0x74, 0x20, 0x6d, 0x79, // [Test my payload] encoding
0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64}; @Test
m_buff = IoBuffer.allocate(overallMessage.length).setAutoExpand(true); public void testBugOnRealCase() throws Exception {
m_buff.put(overallMessage); byte[] overallMessage = new byte[]{0x30, 0x17, //fixed header, 25 byte lenght
m_buff.flip(); 0x00, 0x06, 0x2f, 0x74, 0x6f, 0x70, 0x69, 0x63, //[/topic] string 2 len + 6 content
0x54, 0x65, 0x73, 0x74, 0x20, 0x6d, 0x79, // [Test my payload] encoding
//Exercise 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64};
MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder); m_buff = IoBuffer.allocate(overallMessage.length).setAutoExpand(true);
m_buff.put(overallMessage);
assertNotNull(m_mockProtoDecoder.getMessage()); m_buff.flip();
assertEquals(MessageDecoder.OK, res);
} //Exercise
MessageDecoderResult res = m_msgdec.decode(null, m_buff, m_mockProtoDecoder);
private void initHeader(IoBuffer buff) throws IllegalAccessException {
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic")).flip(); assertNotNull(m_mockProtoDecoder.getMessage());
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4)).put(Utils.encodeRemainingLength(tmp.remaining())); assertEquals(MessageDecoder.OK, res);
//topic name }
buff.put(tmp);
} @Test
public void testDecodeBigContent() throws Exception {
private void initHeaderWithMessageID(IoBuffer buff, int messageID) throws IllegalAccessException { int size = 129;
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic")); IoBuffer payload = TestUtils.generateRandomPayload(size);
Utils.writeWord(tmp, messageID);
tmp.flip(); IoBuffer firstPublish = generatePublishQoS0(payload);
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4 | 0x02)) //set Qos to 1 IoBuffer secondPublish = generatePublishQoS0(TestUtils.generateRandomPayload(size));
.put(Utils.encodeRemainingLength(tmp.remaining()));
//topic name IoBuffer doubleMessageBuf = IoBuffer.allocate(size * 2).setAutoExpand(true);
buff.put(tmp); doubleMessageBuf.put(firstPublish).put(secondPublish).flip();
}
private void initHeaderWithMessageID_Payload(IoBuffer buff, int messageID, byte[] payload) throws IllegalAccessException { //Exercise
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic")); MessageDecoderResult res = m_msgdec.decode(null, doubleMessageBuf, m_mockProtoDecoder);
Utils.writeWord(tmp, messageID);
tmp.put(payload); assertEquals(MessageDecoder.OK, res);
tmp.flip(); PublishMessage pubMsg = m_mockProtoDecoder.getMessage();
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4 | 0x02)) //set Qos to 1 assertNotNull(pubMsg);
.put(Utils.encodeRemainingLength(tmp.remaining()));
//topic name res = m_msgdec.decode(null, doubleMessageBuf, m_mockProtoDecoder);
buff.put(tmp);
} assertNotNull(m_mockProtoDecoder.getMessage());
} assertEquals(MessageDecoder.OK, res);
m_buff.flip();
}
private void initHeader(IoBuffer buff) throws IllegalAccessException {
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic")).flip();
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4)).put(Utils.encodeRemainingLength(tmp.remaining()));
//topic name
buff.put(tmp);
}
private void initHeaderWithMessageID(IoBuffer buff, int messageID) throws IllegalAccessException {
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic"));
Utils.writeWord(tmp, messageID);
tmp.flip();
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4 | 0x02)) //set Qos to 1
.put(Utils.encodeRemainingLength(tmp.remaining()));
//topic name
buff.put(tmp);
}
private void initHeaderWithMessageID_Payload(IoBuffer buff, int messageID, byte[] payload) throws IllegalAccessException {
IoBuffer tmp = IoBuffer.allocate(4).setAutoExpand(true).put(Utils.encodeString("Fake Topic"));
Utils.writeWord(tmp, messageID);
tmp.put(payload);
tmp.flip();
buff.clear().put((byte) (AbstractMessage.PUBLISH << 4 | 0x02)) //set Qos to 1
.put(Utils.encodeRemainingLength(tmp.remaining()));
//topic name
buff.put(tmp);
}
private IoBuffer generatePublishQoS0(IoBuffer payload) throws IllegalAccessException {
int size = payload.capacity();
IoBuffer messageBody = IoBuffer.allocate(size).setAutoExpand(true);
messageBody.put(Utils.encodeString("/topic"));
//ONLY for QoS > 1 Utils.writeWord(messageBody, messageID);
messageBody.put(payload).flip();
IoBuffer completeMsg = IoBuffer.allocate(size).setAutoExpand(true);
completeMsg.clear().put((byte) (AbstractMessage.PUBLISH << 4 | 0x00)) //set Qos to 0
.put(Utils.encodeRemainingLength(messageBody.remaining()));
completeMsg.put(messageBody).flip();
return completeMsg;
}
}
@@ -1,145 +1,167 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import org.dna.mqtt.moquette.proto.PublishEncoder; import org.apache.mina.core.buffer.IoBuffer;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType; import org.dna.mqtt.moquette.proto.messages.AbstractMessage.QOSType;
import org.dna.mqtt.moquette.proto.messages.PublishMessage; import org.dna.mqtt.moquette.proto.messages.PublishMessage;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.dna.mqtt.moquette.proto.TestUtils.*; import static org.dna.mqtt.moquette.proto.TestUtils.*;
/** /**
* *
* @author andrea * @author andrea
*/ */
public class PublishEncoderTest { public class PublishEncoderTest {
PublishEncoder m_encoder; PublishEncoder m_encoder;
TestUtils.MockProtocolEncoderOutput m_mockProtoEncoder; TestUtils.MockProtocolEncoderOutput m_mockProtoEncoder;
@Before @Before
public void setUp() { public void setUp() {
m_encoder = new PublishEncoder(); m_encoder = new PublishEncoder();
m_mockProtoEncoder = new TestUtils.MockProtocolEncoderOutput(); m_mockProtoEncoder = new TestUtils.MockProtocolEncoderOutput();
} }
@Test @Test
public void testEncodeWithQos_0_noMessageID() throws Exception { public void testEncodeWithQos_0_noMessageID() throws Exception {
String topic = "/photos"; String topic = "/photos";
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE); msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic); msg.setTopicName(topic);
//variable part //variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C}; byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload); msg.setPayload(payload);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
//Verify //Verify
assertEquals(0x30, m_mockProtoEncoder.getBuffer().get()); //1 byte assertEquals(0x30, m_mockProtoEncoder.getBuffer().get()); //1 byte
//(2+7) topic + 3 payload //(2+7) topic + 3 payload
assertEquals(12, m_mockProtoEncoder.getBuffer().get()); //remaining length assertEquals(12, m_mockProtoEncoder.getBuffer().get()); //remaining length
//Variable part //Variable part
verifyString(topic, m_mockProtoEncoder.getBuffer()); verifyString(topic, m_mockProtoEncoder.getBuffer());
verifyBuff(payload.length, payload, m_mockProtoEncoder.getBuffer()); verifyBuff(payload.length, payload, m_mockProtoEncoder.getBuffer());
} }
@Test @Test
public void testEncodeWithQos_1_MessageID() throws Exception { public void testEncodeWithQos_1_MessageID() throws Exception {
String topic = "/photos"; String topic = "/photos";
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE); msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1); msg.setMessageID(1);
msg.setTopicName(topic); msg.setTopicName(topic);
//variable part //variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C}; byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload); msg.setPayload(payload);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
//Verify //Verify
assertEquals(0x32, m_mockProtoEncoder.getBuffer().get()); //1 byte assertEquals(0x32, m_mockProtoEncoder.getBuffer().get()); //1 byte
//(2+7) topic + 2 messageID + 3 payload //(2+7) topic + 2 messageID + 3 payload
assertEquals(14, m_mockProtoEncoder.getBuffer().get()); //remaining length assertEquals(14, m_mockProtoEncoder.getBuffer().get()); //remaining length
//Variable part //Variable part
verifyString(topic, m_mockProtoEncoder.getBuffer()); verifyString(topic, m_mockProtoEncoder.getBuffer());
assertEquals(0, m_mockProtoEncoder.getBuffer().get()); //MessageID MSB assertEquals(0, m_mockProtoEncoder.getBuffer().get()); //MessageID MSB
assertEquals(1, m_mockProtoEncoder.getBuffer().get()); //MessageID LSB assertEquals(1, m_mockProtoEncoder.getBuffer().get()); //MessageID LSB
verifyBuff(payload.length, payload, m_mockProtoEncoder.getBuffer()); verifyBuff(payload.length, payload, m_mockProtoEncoder.getBuffer());
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testEncodeWithQos_1_noMessageID_fail() throws Exception { public void testEncodeWithQos_1_noMessageID_fail() throws Exception {
String topic = "pictures/photos"; String topic = "pictures/photos";
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE); msg.setQos(QOSType.LEAST_ONE);
msg.setTopicName(topic); msg.setTopicName(topic);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testEncode_empty_topic() throws Exception { public void testEncode_empty_topic() throws Exception {
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE); msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1); msg.setMessageID(1);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testEncode_null_topic() throws Exception { public void testEncode_null_topic() throws Exception {
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.LEAST_ONE); msg.setQos(QOSType.LEAST_ONE);
msg.setMessageID(1); msg.setMessageID(1);
msg.setTopicName(null); msg.setTopicName(null);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testEncode_qos_reserved() throws Exception { public void testEncode_qos_reserved() throws Exception {
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.RESERVED); msg.setQos(QOSType.RESERVED);
msg.setMessageID(1); msg.setMessageID(1);
msg.setTopicName(null); msg.setTopicName(null);
//variable part //variable part
byte[] payload = new byte[]{0x0A, 0x0B, 0x0C}; byte[] payload = new byte[]{0x0A, 0x0B, 0x0C};
msg.setPayload(payload); msg.setPayload(payload);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
} }
@Test @Test
public void testBugMissedMessageIDEncoding() throws Exception{ public void testBugMissedMessageIDEncoding() throws Exception{
String topic = "/topic"; String topic = "/topic";
PublishMessage msg = new PublishMessage(); PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE); msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic); msg.setTopicName(topic);
//variable part //variable part
byte[] payload = "Test my payload".getBytes(); byte[] payload = "Test my payload".getBytes();
msg.setPayload(payload); msg.setPayload(payload);
//Exercise //Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder); m_encoder.encode(null, msg, m_mockProtoEncoder);
//Verify //Verify
//2 byte header + (2+6) topic name + [2 message ID if QoQ <> 0]+ payload //2 byte header + (2+6) topic name + [2 message ID if QoQ <> 0]+ payload
int size = m_mockProtoEncoder.getBuffer().remaining(); int size = m_mockProtoEncoder.getBuffer().remaining();
assertEquals(10+payload.length, size); assertEquals(10+payload.length, size);
assertEquals(0x30, m_mockProtoEncoder.getBuffer().get()); //1 byte assertEquals(0x30, m_mockProtoEncoder.getBuffer().get()); //1 byte
assertEquals(23, m_mockProtoEncoder.getBuffer().get()); //1 byte the length assertEquals(23, m_mockProtoEncoder.getBuffer().get()); //1 byte the length
} }
}
@Test
public void testEncodeBigMessage() throws Exception {
byte[] payload = new byte[256];
TestUtils.generateRandomPayload(256).get(payload);
String topic = "/topic";
PublishMessage msg = new PublishMessage();
msg.setQos(QOSType.MOST_ONE);
msg.setTopicName(topic);
//variable part
msg.setPayload(payload);
//Exercise
m_encoder.encode(null, msg, m_mockProtoEncoder);
int size = m_mockProtoEncoder.getBuffer().remaining();
assertEquals(11+payload.length, size);
assertEquals(0x30, m_mockProtoEncoder.getBuffer().get()); //1 byte
}
}
@@ -1,97 +1,124 @@
package org.dna.mqtt.moquette.proto; package org.dna.mqtt.moquette.proto;
import java.io.UnsupportedEncodingException; import java.io.File;
import org.apache.mina.core.buffer.IoBuffer; import java.io.FileInputStream;
import org.apache.mina.core.filterchain.IoFilter.NextFilter; import java.io.FileNotFoundException;
import org.apache.mina.core.future.WriteFuture; import java.io.IOException;
import org.apache.mina.core.session.IoSession; import java.io.UnsupportedEncodingException;
import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.core.filterchain.IoFilter.NextFilter;
import org.dna.mqtt.moquette.proto.messages.AbstractMessage; import org.apache.mina.core.future.WriteFuture;
import static org.junit.Assert.*; import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
/** import org.apache.mina.filter.codec.ProtocolEncoderOutput;
* import org.dna.mqtt.moquette.proto.messages.AbstractMessage;
* @author andrea import static org.junit.Assert.*;
*/
public class TestUtils { /**
*
static final class MockProtocolDecoderOutput<T extends AbstractMessage> implements ProtocolDecoderOutput { * @author andrea
*/
protected T m_connMessage; public class TestUtils {
public T getMessage() { static final class MockProtocolDecoderOutput<T extends AbstractMessage> implements ProtocolDecoderOutput {
return m_connMessage;
} protected T m_connMessage;
public void write(Object message) { public T getMessage() {
m_connMessage = (T) message; return m_connMessage;
} }
public void flush(NextFilter nextFilter, IoSession session) { public void write(Object message) {
throw new UnsupportedOperationException("Not supported yet."); m_connMessage = (T) message;
} }
}
public void flush(NextFilter nextFilter, IoSession session) {
throw new UnsupportedOperationException("Not supported yet.");
static final class MockProtocolEncoderOutput implements ProtocolEncoderOutput { }
}
protected IoBuffer m_buffer;
public IoBuffer getBuffer() { static final class MockProtocolEncoderOutput implements ProtocolEncoderOutput {
return m_buffer;
} protected IoBuffer m_buffer;
public void write(Object buffer) { public IoBuffer getBuffer() {
m_buffer = (IoBuffer) buffer; return m_buffer;
} }
public void mergeAll() { public void write(Object buffer) {
throw new UnsupportedOperationException("Not supported yet."); m_buffer = (IoBuffer) buffer;
} }
public WriteFuture flush() { public void mergeAll() {
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
}
public WriteFuture flush() {
/** throw new UnsupportedOperationException("Not supported yet.");
* Verify the presence of the given string starting from the current position }
* inside the buffer. }
*/
static void verifyString(String str, IoBuffer buff) throws UnsupportedEncodingException { /**
IoBuffer tmpBuff = IoBuffer.allocate(2).setAutoExpand(true); * Verify the presence of the given string starting from the current position
byte[] raw = str.getBytes("UTF-8"); * inside the buffer.
Utils.writeWord(tmpBuff, raw.length); */
tmpBuff.put(raw).flip(); static void verifyString(String str, IoBuffer buff) throws UnsupportedEncodingException {
IoBuffer tmpBuff = IoBuffer.allocate(2).setAutoExpand(true);
verifyIoBuffer(tmpBuff, buff); byte[] raw = str.getBytes("UTF-8");
} Utils.writeWord(tmpBuff, raw.length);
tmpBuff.put(raw).flip();
static void verifyIoBuffer(IoBuffer expected, IoBuffer found) {
while(expected.hasRemaining()) { verifyIoBuffer(tmpBuff, buff);
assertEquals(expected.get(), found.get()); }
}
} static void verifyIoBuffer(IoBuffer expected, IoBuffer found) {
while(expected.hasRemaining()) {
/** assertEquals(expected.get(), found.get());
* Verify that the given bytes buffer og the given numBytes length is present }
* in the buff starting from the current position. }
*/
static void verifyBuff(int numBytes, byte[] bytes, IoBuffer buff) { /**
assertTrue(numBytes <= buff.remaining()); * Verify that the given bytes buffer og the given numBytes length is present
byte[] toCheck = new byte[numBytes]; * in the buff starting from the current position.
buff.get(toCheck); */
static void verifyBuff(int numBytes, byte[] bytes, IoBuffer buff) {
for (int i = 0; i < numBytes; i++) { assertTrue(numBytes <= buff.remaining());
assertEquals(bytes[i], toCheck[i]); byte[] toCheck = new byte[numBytes];
} buff.get(toCheck);
}
for (int i = 0; i < numBytes; i++) {
static void verifyEquals(byte[] expected, byte[] found) { assertEquals(bytes[i], toCheck[i]);
assertEquals(expected.length, found.length); }
for (int i = 0; i < expected.length; i++) { }
assertEquals(expected[i], found[i]);
} static void verifyEquals(byte[] expected, byte[] found) {
} assertEquals(expected.length, found.length);
} for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], found[i]);
}
}
public static IoBuffer generateRandomPayload(int size) {
IoBuffer payloadBuffer = IoBuffer.allocate(size);
for (int i = 0; i < size; i++) {
payloadBuffer.put((byte) (Math.random() * 255));
}
payloadBuffer.flip();
return payloadBuffer;
}
public static IoBuffer loadFile(String path) throws FileNotFoundException, IOException {
File f = new File(path);
long size = f.length();
byte[] payload = new byte[(int) size];
FileInputStream fr = new FileInputStream(f);
fr.read(payload);
IoBuffer payloadBuffer = IoBuffer.allocate((int) size);
payloadBuffer.put(payload).flip();
return payloadBuffer;
}
}