Revision control

Copy as Markdown

// Copyright (C) 2018 Witekio.
// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest>
#include <QCoreApplication>
#include <private/qcoapinternalreply_p.h>
#include <private/qcoapreply_p.h>
class tst_QCoapInternalReply : public QObject
{
Q_OBJECT
private Q_SLOTS:
void parseReplyPdu_data();
void parseReplyPdu();
void parseInvalidReplyPdu_data();
void parseInvalidReplyPdu();
void parseExtendedOption();
void parseEmptyBlockOption();
void nextBlockToSendWithEmptyBlock1();
void updateReply_data();
void updateReply();
};
void tst_QCoapInternalReply::parseReplyPdu_data()
{
QTest::addColumn<QtCoap::ResponseCode>("responseCode");
QTest::addColumn<QCoapMessage::Type>("type");
QTest::addColumn<quint16>("messageId");
QTest::addColumn<QByteArray>("token");
QTest::addColumn<quint8>("tokenLength");
QTest::addColumn<QList<QCoapOption::OptionName>>("optionsNames");
QTest::addColumn<QList<quint8>>("optionsLengths");
QTest::addColumn<QList<QByteArray>>("optionsValues");
QTest::addColumn<QString>("payload");
QTest::addColumn<QString>("pduHexa");
QList<QCoapOption::OptionName> optionsNamesReply({QCoapOption::ContentFormat,
QCoapOption::MaxAge});
QList<quint8> optionsLengthsReply({0, 1});
QList<QByteArray> optionsValuesReply({"", QByteArray::fromHex("1e")});
QList<QCoapOption::OptionName> bigOptionNameReply({QCoapOption::Size1});
QList<quint8> bigOptionLengthReply({26});
QList<QByteArray> bigOptionValueReply({QByteArray("abcdefghijklmnopqrstuvwxyz")});
QTest::newRow("reply_with_options_and_payload")
<< QtCoap::ResponseCode::Content
<< QCoapMessage::Type::NonConfirmable
<< quint16(64463)
<< QByteArray("4647f09b")
<< quint8(4)
<< optionsNamesReply
<< optionsLengthsReply
<< optionsValuesReply
<< "Type: 1 (NON)\nCode: 1 (GET)\nMID: 56400\nToken: 4647f09b"
<< "5445fbcf4647f09bc0211eff547970653a203120284e4f4e290a436f64653a20"
"312028474554290a4d49443a2035363430300a546f6b656e3a20343634376630"
"3962";
QTest::newRow("reply_with_payload")
<< QtCoap::ResponseCode::Content
<< QCoapMessage::Type::NonConfirmable
<< quint16(64463)
<< QByteArray("4647f09b")
<< quint8(4)
<< QList<QCoapOption::OptionName>()
<< QList<quint8>()
<< QList<QByteArray>()
<< "Type: 1 (NON)\nCode: 1 (GET)\nMID: 56400\nToken: 4647f09b"
<< "5445fbcf4647f09bff547970653a203120284e4f4e290a436f64653a20312028"
"474554290a4d49443a2035363430300a546f6b656e3a203436343766303962";
QTest::newRow("reply_with_options")
<< QtCoap::ResponseCode::Content
<< QCoapMessage::Type::NonConfirmable
<< quint16(64463)
<< QByteArray("4647f09b")
<< quint8(4)
<< optionsNamesReply
<< optionsLengthsReply
<< optionsValuesReply
<< ""
<< "5445fbcf4647f09bc0211e";
QTest::newRow("reply_only")
<< QtCoap::ResponseCode::Content
<< QCoapMessage::Type::NonConfirmable
<< quint16(64463)
<< QByteArray("4647f09b")
<< quint8(4)
<< QList<QCoapOption::OptionName>()
<< QList<quint8>()
<< QList<QByteArray>()
<< ""
<< "5445fbcf4647f09b";
QTest::newRow("reply_with_big_option")
<< QtCoap::ResponseCode::Content
<< QCoapMessage::Type::NonConfirmable
<< quint16(64463)
<< QByteArray("4647f09b")
<< quint8(4)
<< bigOptionNameReply
<< bigOptionLengthReply
<< bigOptionValueReply
<< ""
<< "5445fbcf4647f09bdd2f0d6162636465666768696a6b6c6d6e6f707172737475"
"767778797a";
}
void tst_QCoapInternalReply::parseReplyPdu()
{
QFETCH(QtCoap::ResponseCode, responseCode);
QFETCH(QCoapMessage::Type, type);
QFETCH(quint16, messageId);
QFETCH(QByteArray, token);
QFETCH(quint8, tokenLength);
QFETCH(QList<QCoapOption::OptionName>, optionsNames);
QFETCH(QList<quint8>, optionsLengths);
QFETCH(QList<QByteArray>, optionsValues);
QFETCH(QString, payload);
QFETCH(QString, pduHexa);
QScopedPointer<QCoapInternalReply>
reply(QCoapInternalReply::createFromFrame(QByteArray::fromHex(pduHexa.toUtf8())));
QCOMPARE(reply->message()->type(), type);
QCOMPARE(reply->message()->tokenLength(), tokenLength);
QCOMPARE(reply->responseCode(), responseCode);
QCOMPARE(reply->message()->messageId(), messageId);
QCOMPARE(reply->message()->token().toHex(), token);
QCOMPARE(reply->message()->optionCount(), optionsNames.size());
for (int i = 0; i < reply->message()->optionCount(); ++i) {
QCoapOption option = reply->message()->optionAt(i);
QCOMPARE(option.name(), optionsNames.at(i));
QCOMPARE(option.length(), optionsLengths.at(i));
QCOMPARE(option.opaqueValue(), optionsValues.at(i));
}
QCOMPARE(reply->message()->payload(), payload.toUtf8());
}
void tst_QCoapInternalReply::parseInvalidReplyPdu_data()
{
QTest::addColumn<QByteArray>("frame");
// A CoAP message is at least 4 bytes long (the fixed header). Frames shorter
// than that must be rejected before the header is dereferenced;
// createFromFrame() signals rejection by returning nullptr.
QTest::newRow("empty") << QByteArray();
QTest::newRow("one_byte") << QByteArray::fromHex("50");
QTest::newRow("two_bytes") << QByteArray::fromHex("5045");
QTest::newRow("three_bytes") << QByteArray::fromHex("5045fb");
// Option-parser cases: each frame has a valid 4-byte header (no token)
// followed by a single option byte whose encoding demands bytes that are
// not present. createFromFrame() must reject the frame rather than read
// past the buffer or deliver a silently truncated option.
// 0x09: option value length 9, but no value bytes follow it.
QTest::newRow("option_value_overruns_buffer") << QByteArray::fromHex("50450001" "09");
// 0xd0: delta nibble 13 promises one extended delta byte that is missing.
QTest::newRow("missing_option_delta_ext_byte") << QByteArray::fromHex("50450001" "d0");
// 0x0d: length nibble 13 promises one extended length byte that is missing.
QTest::newRow("missing_option_length_ext_byte") << QByteArray::fromHex("50450001" "0d");
// 0x0d + 0xff: extended length yields optionLength 268, far past the buffer.
QTest::newRow("extended_option_length_overruns") << QByteArray::fromHex("50450001" "0d" "ff");
// 0xe0 + 0xffff: extended delta 0xffff -> option number 65535 + 269 = 65804,
// which exceeds the 65535 maximum, so the frame is rejected.
QTest::newRow("option_number_exceeds_max") << QByteArray::fromHex("50450001" "e0" "ffff");
// Token-length cases.
// TKL 9 is reserved (RFC 7252 §3) and must be rejected even when the 9 token
// bytes are present, so this frame includes them.
QTest::newRow("reserved_token_length") << QByteArray::fromHex("59450001" "aabbccddeeff001122");
// TKL 4 is valid, but no token bytes follow the 4-byte header.
QTest::newRow("truncated_token") << QByteArray::fromHex("54450001");
// RFC 7959 §2.2: SZX value 7 (block size 2048) is reserved. Reject a block
// option whose last byte has SZX 7 (low 3 bits set). Value byte 0x07 encodes
// NUM=0, M=0, SZX=7. Option header 0xd1 = delta 13 (extended), length 1.
// Block2 (number 23): extended delta byte 0x0a -> 10 + 13 = 23.
QTest::newRow("reserved_block2_szx7") << QByteArray::fromHex("50450001" "d10a" "07");
// Block1 (number 27): extended delta byte 0x0e -> 14 + 13 = 27.
QTest::newRow("reserved_block1_szx7") << QByteArray::fromHex("50450001" "d10e" "07");
// RFC 7959 §2.2: a block option value is 0-3 bytes; a 4-byte value is rejected
// (it would overflow the 20-bit block number). Option header 0xd4 = delta 13
// (extended), length 4; 0x0a -> Block2 (23); last byte 0x04 has a valid SZX, so
// this exercises the length check rather than the SZX check.
QTest::newRow("oversized_block2_value") << QByteArray::fromHex("50450001" "d40a" "01020304");
}
void tst_QCoapInternalReply::parseInvalidReplyPdu()
{
QFETCH(QByteArray, frame);
QScopedPointer<QCoapInternalReply> reply(QCoapInternalReply::createFromFrame(frame));
QVERIFY2(reply.isNull(),
"createFromFrame() must reject a malformed frame by returning nullptr");
}
void tst_QCoapInternalReply::parseExtendedOption()
{
// RFC 7252 §3.1: a delta/length nibble of 14 is followed by a 16-bit
// unsigned integer in network byte order (the value minus 269). Build one
// option that uses the 16-bit form for BOTH its delta and its length, with
// distinct first/second bytes so the test also verifies the byte order:
// 0xee -> delta nibble 14, length nibble 14
// 0x01 0x02 -> extended delta = 0x0102 = 258 -> option number 258 + 269 = 527
// 0x00 0x03 -> extended length = 0x0003 = 3 -> value length 3 + 269 = 272
// 272 value bytes
// A decoder that read one byte instead of two, or used the wrong byte order,
// would compute different numbers and misalign the rest of the frame.
const QByteArray value(272, 'x');
const QByteArray frame = QByteArray::fromHex("50450001" "ee" "0102" "0003") + value;
QScopedPointer<QCoapInternalReply> reply(QCoapInternalReply::createFromFrame(frame));
QVERIFY(!reply.isNull());
QCOMPARE(reply->message()->optionCount(), 1);
const QCoapOption option = reply->message()->options().constFirst();
QCOMPARE(static_cast<int>(option.name()), 527);
QCOMPARE(option.opaqueValue(), value);
}
void tst_QCoapInternalReply::parseEmptyBlockOption()
{
// RFC 7959 §2.2: a zero-length Block option is valid and encodes
// NUM=0, M=0, SZX=0. Frame: 4-byte header (no token), then a Block2
// option (number 23) carrying an empty value.
// 0xd0 -> option delta nibble 13 (extended), length nibble 0
// 0x0a -> extended delta byte, giving option number 13 + 10 = 23 (Block2)
const QByteArray frame = QByteArray::fromHex("50450001" "d0" "0a");
QScopedPointer<QCoapInternalReply> reply(QCoapInternalReply::createFromFrame(frame));
QVERIFY(!reply.isNull());
QCOMPARE(reply->currentBlockNumber(), 0u);
QVERIFY(!reply->hasMoreBlocksToReceive());
QCOMPARE(reply->blockSize(), 16u);
}
void tst_QCoapInternalReply::nextBlockToSendWithEmptyBlock1()
{
// A zero-length Block1 option is valid (RFC 7959 §2.2), encoding M=0, so
// there is no next block to send. nextBlockToSend() must return -1 rather
// than read optionData[-1]. Frame: 4-byte header, then a Block1 option
// (number 27) with an empty value.
// 0xd0 -> option delta nibble 13 (extended), length nibble 0
// 0x0e -> extended delta byte, giving option number 13 + 14 = 27 (Block1)
const QByteArray frame = QByteArray::fromHex("50450001" "d0" "0e");
QScopedPointer<QCoapInternalReply> reply(QCoapInternalReply::createFromFrame(frame));
QVERIFY(!reply.isNull());
QCOMPARE(reply->nextBlockToSend(), -1);
}
void tst_QCoapInternalReply::updateReply_data()
{
QTest::addColumn<QByteArray>("data");
QTest::newRow("success") << QByteArray("Data for the updating test");
}
void tst_QCoapInternalReply::updateReply()
{
QFETCH(QByteArray, data);
QScopedPointer<QCoapReply> reply(QCoapReplyPrivate::createCoapReply(QCoapRequest()));
QCoapInternalReply internalReply;
internalReply.message()->setPayload(data);
QSignalSpy spyReplyFinished(reply.data(), &QCoapReply::finished);
QMetaObject::invokeMethod(reply.data(), "_q_setContent",
Q_ARG(QHostAddress, internalReply.senderAddress()),
Q_ARG(QCoapMessage, *internalReply.message()),
Q_ARG(QtCoap::ResponseCode, internalReply.responseCode()));
QMetaObject::invokeMethod(reply.data(), "_q_setFinished", Q_ARG(QtCoap::Error, QtCoap::Error::Ok));
QTRY_COMPARE_WITH_TIMEOUT(spyReplyFinished.size(), 1, 1000);
QCOMPARE(reply->readAll(), data);
}
QTEST_MAIN(tst_QCoapInternalReply)
#include "tst_qcoapinternalreply.moc"