/*! * * Copyright (c) 2026 Diality Inc. - All Rights Reserved. * \copyright * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * \file MqttPacket.cpp * \author (original) Stephen Quong * \date (original) 30-Jul-2026 * */ #include "MqttPacket.h" namespace Mqtt { constexpr int MAX_REMAINING_LENGTH_BYTES = 4; constexpr quint8 PROTOCOL_LEVEL_311 = 0x04; constexpr quint8 CONNECT_FLAG_CLEAN_SESSION = 0x02; /*! * \brief encodeRemainingLength * \details Encodes a length as the MQTT variable-length field: seven value bits * per byte, high bit set while more bytes follow. * \param length Byte count of everything after the fixed header. * \return One to four encoded bytes. */ QByteArray encodeRemainingLength(quint32 length) { QByteArray out; do { quint8 byte = static_cast(length % 128); length /= 128; if (length > 0) { byte = static_cast(byte | 0x80); } out.append(static_cast(byte)); } while (length > 0); return out; } /*! * \brief decodeRemainingLength * \details Reads the variable-length field starting at offset. * \param bytes Buffer to read from. * \param offset Index of the first length byte. * \param value Receives the decoded length. * \return Bytes consumed, 0 when more input is needed, or -1 when malformed. */ int decodeRemainingLength(const QByteArray &bytes, int offset, quint32 &value) { quint32 multiplier = 1; value = 0; for (int i = 0; i < MAX_REMAINING_LENGTH_BYTES; ++i) { if (offset + i >= bytes.size()) { return 0; } const quint8 byte = static_cast(bytes.at(offset + i)); value += static_cast(byte & 0x7F) * multiplier; if ((byte & 0x80) == 0) { return i + 1; } multiplier *= 128; } return -1; } /*! * \brief appendString * \details Appends a UTF-8 string in MQTT form: a two-byte big-endian length * followed by the encoded bytes. * \param out Buffer to append to. * \param value String to encode. */ void appendString(QByteArray &out, const QString &value) { const QByteArray utf8 = value.toUtf8(); out.append(static_cast((utf8.size() >> 8) & 0xFF)); out.append(static_cast(utf8.size() & 0xFF)); out.append(utf8); } /*! * \brief readString * \details Reads one length-prefixed UTF-8 string and advances pos past it. * \param bytes Buffer to read from. * \param pos Read cursor, advanced on success. * \param end One past the last readable index. * \param out Receives the decoded string. * \return true if a complete string was read. */ bool readString(const QByteArray &bytes, int &pos, int end, QString &out) { if (pos + 2 > end) { return false; } const int length = (static_cast(bytes.at(pos)) << 8) | static_cast(bytes.at(pos + 1)); pos += 2; if (pos + length > end) { return false; } out = QString::fromUtf8(bytes.constData() + pos, length); pos += length; return true; } /*! * \brief readUint16 * \details Reads one big-endian 16-bit value and advances pos past it. * \param bytes Buffer to read from. * \param pos Read cursor, advanced on success. * \param end One past the last readable index. * \param out Receives the decoded value. * \return true if two bytes were available. */ bool readUint16(const QByteArray &bytes, int &pos, int end, quint16 &out) { if (pos + 2 > end) { return false; } out = static_cast((static_cast(bytes.at(pos)) << 8) | static_cast(bytes.at(pos + 1))); pos += 2; return true; } /*! * \brief buildFixedHeader * \details Prepends the fixed header to an already-built packet body. * \param type Control packet type. * \param flags Low nibble of byte one. * \param body Variable header and payload. * \return The complete packet. */ QByteArray buildFixedHeader(Mqtt::PacketType type, quint8 flags, const QByteArray &body) { QByteArray out; out.append(static_cast((static_cast(type) << 4) | (flags & 0x0F))); out.append(encodeRemainingLength(static_cast(body.size()))); out.append(body); return out; } } // namespace namespace Mqtt { /*! * \brief Mqtt::buildConnect * \details Builds a CONNECT for a publish-only session: no will, no credentials. * \param clientId Client identifier; must match the IoT Thing name on AWS. * \param keepAliveSecs Keep-alive interval advertised to the broker. * \param cleanSession True to ask the broker to retain no session state. * \return The encoded packet. */ QByteArray buildConnect(const QString &clientId, quint16 keepAliveSecs, bool cleanSession) { QByteArray body; appendString(body, QStringLiteral("MQTT")); body.append(static_cast(PROTOCOL_LEVEL_311)); body.append(static_cast(cleanSession ? CONNECT_FLAG_CLEAN_SESSION : 0x00)); body.append(static_cast((keepAliveSecs >> 8) & 0xFF)); body.append(static_cast(keepAliveSecs & 0xFF)); appendString(body, clientId); return buildFixedHeader(PacketType::Connect, 0, body); } /*! * \brief Mqtt::buildConnAck * \details Builds the broker's response to a CONNECT. * \param code Acceptance or the reason for refusal. * \param sessionPresent True when the broker resumed an existing session. * \return The encoded packet. */ QByteArray buildConnAck(ConnAckCode code, bool sessionPresent) { QByteArray body; body.append(static_cast(sessionPresent ? 0x01 : 0x00)); body.append(static_cast(code)); return buildFixedHeader(PacketType::ConnAck, 0, body); } /*! * \brief Mqtt::buildPublish * \details Builds a PUBLISH. The packet identifier is only present at QoS > 0. * \param topic Fully resolved topic name. * \param payload Application bytes, passed through untouched. * \param qos Delivery quality, 0 or 1. * \param packetId Identifier to correlate the PUBACK; ignored at QoS 0. * \return The encoded packet. */ QByteArray buildPublish(const QString &topic, const QByteArray &payload, quint8 qos, quint16 packetId) { QByteArray body; appendString(body, topic); if (qos > 0) { body.append(static_cast((packetId >> 8) & 0xFF)); body.append(static_cast(packetId & 0xFF)); } body.append(payload); return buildFixedHeader(PacketType::Publish, static_cast((qos & 0x03) << 1), body); } /*! * \brief Mqtt::buildPubAck * \details Builds the QoS 1 acknowledgement for a received PUBLISH. * \param packetId Identifier copied from the PUBLISH being acknowledged. * \return The encoded packet. */ QByteArray buildPubAck(quint16 packetId) { QByteArray body; body.append(static_cast((packetId >> 8) & 0xFF)); body.append(static_cast(packetId & 0xFF)); return buildFixedHeader(PacketType::PubAck, 0, body); } /*! * \brief Mqtt::buildPingReq * \details Builds the keep-alive request. * \return The encoded packet. */ QByteArray buildPingReq() { return buildFixedHeader(PacketType::PingReq, 0, QByteArray()); } /*! * \brief Mqtt::buildPingResp * \details Builds the keep-alive response. * \return The encoded packet. */ QByteArray buildPingResp() { return buildFixedHeader(PacketType::PingResp, 0, QByteArray()); } /*! * \brief Mqtt::buildDisconnect * \details Builds the graceful shutdown notification. * \return The encoded packet. */ QByteArray buildDisconnect() { return buildFixedHeader(PacketType::Disconnect, 0, QByteArray()); } /*! * \brief Mqtt::readPacket * \details Decodes one packet from the front of bytes, consuming it on success. * \param bytes Accumulated stream data; modified in place. * \param packet Receives the decoded packet. * \return Complete when a packet was consumed, Incomplete when more data is * needed, or Error when the stream is malformed. */ ReadState readPacket(QByteArray &bytes, Packet &packet) { if (bytes.size() < 2) { return ReadState::Incomplete; } const quint8 header = static_cast(bytes.at(0)); const quint8 rawType = static_cast(header >> 4); const quint8 flags = static_cast(header & 0x0F); quint32 remainingLength = 0; const int lengthBytes = decodeRemainingLength(bytes, 1, remainingLength); if (lengthBytes == 0) { return ReadState::Incomplete; } if (lengthBytes < 0) { return ReadState::Error; } const int headerSize = 1 + lengthBytes; const int totalSize = headerSize + static_cast(remainingLength); if (bytes.size() < totalSize) { return ReadState::Incomplete; } packet = Packet(); packet.type = static_cast(rawType); int pos = headerSize; const int end = totalSize; bool ok = true; switch (packet.type) { case PacketType::Connect: { QString protocolName; ok = readString(bytes, pos, end, protocolName) && protocolName == QStringLiteral("MQTT"); if (ok && pos + 2 <= end) { // Protocol level is validated by the caller, which decides the CONNACK code. pos++; packet.cleanSession = (static_cast(bytes.at(pos)) & CONNECT_FLAG_CLEAN_SESSION) != 0; pos++; ok = readUint16(bytes, pos, end, packet.keepAliveSecs) && readString(bytes, pos, end, packet.clientId); } else { ok = false; } break; } case PacketType::ConnAck: { if (pos + 2 <= end) { packet.sessionPresent = (static_cast(bytes.at(pos)) & 0x01) != 0; packet.returnCode = static_cast(static_cast(bytes.at(pos + 1))); pos += 2; } else { ok = false; } break; } case PacketType::Publish: { packet.dup = (flags & 0x08) != 0; packet.qos = static_cast((flags >> 1) & 0x03); packet.retain = (flags & 0x01) != 0; ok = readString(bytes, pos, end, packet.topic); if (ok && packet.qos > 0) { ok = readUint16(bytes, pos, end, packet.packetId); } if (ok) { packet.payload = bytes.mid(pos, end - pos); pos = end; } break; } case PacketType::PubAck: ok = readUint16(bytes, pos, end, packet.packetId); break; case PacketType::PingReq: case PacketType::PingResp: case PacketType::Disconnect: break; default: ok = false; break; } if (!ok) { return ReadState::Error; } bytes.remove(0, totalSize); return ReadState::Complete; } /*! * \brief Mqtt::typeName * \details Maps a packet type to its specification name, for logging. * \param type Packet type to name. * \return The name, or "UNKNOWN" for a type this codec does not handle. */ QString typeName(PacketType type) { switch (type) { case PacketType::Connect: return QStringLiteral("CONNECT"); case PacketType::ConnAck: return QStringLiteral("CONNACK"); case PacketType::Publish: return QStringLiteral("PUBLISH"); case PacketType::PubAck: return QStringLiteral("PUBACK"); case PacketType::PingReq: return QStringLiteral("PINGREQ"); case PacketType::PingResp: return QStringLiteral("PINGRESP"); case PacketType::Disconnect: return QStringLiteral("DISCONNECT"); } return QStringLiteral("UNKNOWN"); } } // namespace Mqtt