Coverage for zenmqtt/mqtt/packet.py: 100%
99 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
1import struct
2from dataclasses import dataclass
3from enum import IntEnum
4from typing import Final, Tuple, Union
6from zenmqtt.exceptions import MalformedPacketError
9class PacketType(IntEnum):
10 RESERVED = 0x00
12 CONNECT = 0x01 # Client to Server | Connection request
13 CONNACK = 0x02 # Server to Client | Connect acknowledgment
15 PUBLISH = 0x03 # Client to Server or Server to Client | Publish message
16 PUBACK = (
17 0x04 # Client to Server or Server to Client | Publish acknowledgment (QoS 1)
18 )
19 PUBREC = 0x05 # Client to Server or Server to Client | Publish received (QoS 2 delivery part 1)
20 PUBREL = 0x06 # Client to Server or Server to Client | Publish release (QoS 2 delivery part 2)
21 PUBCOMP = 0x07 # Client to Server or Server to Client | Publish complete (QoS 2 delivery part 3)
23 SUBSCRIBE = 0x08 # Client to Server | Subscribe request
24 SUBACK = 0x09 # Server to Client | Subscribe acknowledgment
25 UNSUBSCRIBE = 0x0A # Client to Server | Unsubscribe request
26 UNSUBACK = 0x0B # Server to Client | Unsubscribe acknowledgment
28 PINGREQ = 0x0C # Client to Server | PING request
29 PINGRESP = 0x0D # Server to Client | PONG request
31 DISCONNECT = 0x0E # Client to Server or Server to Client | Disconnect notification
33 AUTH = 0x0F # Client to Server or Server to Client | Authentication exchange
36# MQTT Control Packet
37# +-------------------------------------------------------+
38# | Fixed Header, present in all MQTT Control Packets |
39# +-------------------------------------------------------+
40# | Variable Header, present in some MQTT Control Packets |
41# +-------------------------------------------------------+
42# | Payload, present in some MQTT Control Packets |
43# +-------------------------------------------------------+
46FIXED_HEADER_SIZE: Final[int] = 1
48# "Remaining Length" is encoded in at most 4 bytes
49_MAX_VARIABLE_BYTE_INTEGER_SIZE: Final[int] = 4
51_UINT16: Final[struct.Struct] = struct.Struct("!H")
52_UINT32: Final[struct.Struct] = struct.Struct("!L")
55@dataclass(frozen=True)
56class FixedHeader:
57 __slots__ = ("packet_type", "flags", "length")
59 packet_type: PacketType
60 flags: int
61 length: int
64def parse_variable_byte_integer(
65 data: Union[bytes, bytearray], offset: int = 0
66) -> Tuple[int, int]:
67 """
68 Returns the value and the number of bytes it takes.
70 :raises IndexError: data ends before the end of the value
71 :raises MalformedPacketError: the value takes more than 4 bytes
72 """
73 value = 0
74 multiplier = 1
76 for size in range(1, _MAX_VARIABLE_BYTE_INTEGER_SIZE + 1):
77 byte = data[offset + size - 1]
78 value += (byte & 0x7F) * multiplier
80 if byte & 0x80 == 0:
81 return value, size
83 multiplier *= 128
85 raise MalformedPacketError("Variable byte integer is longer than 4 bytes")
88def parse_fixed_header(data: bytes) -> Tuple[FixedHeader, int]:
89 """Returns the fixed header and its size."""
90 try:
91 length, size = parse_variable_byte_integer(data, 1)
92 except IndexError as exc:
93 raise MalformedPacketError("Incomplete fixed header") from exc
95 header = FixedHeader(
96 packet_type=PacketType(data[0] >> 4), flags=data[0] & 0xF, length=length
97 )
99 return header, FIXED_HEADER_SIZE + size
102def split_packet(data: bytes) -> Tuple[FixedHeader, "BytesReader"]:
103 """Splits a whole packet into the fixed header and a reader of the body."""
104 header, header_size = parse_fixed_header(data)
106 if len(data) != header_size + header.length:
107 raise MalformedPacketError(
108 f"Packet size {len(data)} doesn't match {header_size + header.length}"
109 )
111 return header, BytesReader(data[header_size:])
114class BytesReader:
115 """
116 Reads values from the body of a packet. Reading past the end means that
117 the packet is malformed.
118 """
120 __slots__ = ("_data", "_offset")
122 def __init__(self, data: bytes) -> None:
123 self._data = data
124 self._offset = 0
126 def remaining(self) -> int:
127 return len(self._data) - self._offset
129 def read(self, size: int) -> bytes:
130 start = self._offset
131 end = start + size
133 if end > len(self._data):
134 raise MalformedPacketError("Unexpected end of the packet")
136 self._offset = end
137 return self._data[start:end]
139 def read_byte(self) -> int:
140 if self._offset >= len(self._data):
141 raise MalformedPacketError("Unexpected end of the packet")
143 value = self._data[self._offset]
144 self._offset += 1
145 return value
147 def read_uint16(self) -> int:
148 value: int = _UINT16.unpack(self.read(2))[0]
149 return value
151 def read_uint32(self) -> int:
152 value: int = _UINT32.unpack(self.read(4))[0]
153 return value
155 def read_variable_byte_integer(self) -> int:
156 try:
157 value, size = parse_variable_byte_integer(self._data, self._offset)
158 except IndexError as exc:
159 raise MalformedPacketError("Unexpected end of the packet") from exc
161 self._offset += size
162 return value
164 def read_binary(self) -> bytes:
165 """Binary data with 2 bytes of length."""
166 return self.read(self.read_uint16())
168 def read_str(self) -> str:
169 """UTF-8 string with 2 bytes of length."""
170 try:
171 return self.read_binary().decode()
172 except UnicodeDecodeError as exc:
173 raise MalformedPacketError("Invalid UTF-8 string") from exc
175 def read_rest(self) -> bytes:
176 return self.read(self.remaining())
178 def ensure_end(self) -> None:
179 if self.remaining():
180 raise MalformedPacketError(
181 f"{self.remaining()} unexpected bytes at the end of the packet"
182 )