Lines 100.00% 28 / 28
Functions and Methods 100.00% 7 / 7
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
PacketEncoder 100.00% 28 / 28 100.00% 7 / 7 13 100.00% 1 / 1
 encodeConnect 100.00% 16 / 16 100.00% 1 / 1 5
 encodePublish 100.00% 5 / 5 100.00% 1 / 1 3
 encodePubAck 100.00% 1 / 1 100.00% 1 / 1 1
 encodeSubscribe 100.00% 2 / 2 100.00% 1 / 1 1
 encodeUnsubscribe 100.00% 2 / 2 100.00% 1 / 1 1
 encodePingRequest 100.00% 1 / 1 100.00% 1 / 1 1
 encodeDisconnect 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Mqtt\Packet;
6
7use SensitiveParameter;
8use Stewart\Contracts\Mqtt\MqttMessage;
9use Stewart\Contracts\Mqtt\MqttQos;
10use Stewart\Contracts\Time\Duration;
11
12final readonly class PacketEncoder
13{
14    private const string PROTOCOL_NAME = 'MQTT';
15
16    private const int PROTOCOL_LEVEL = 4;
17
18    private const int CLEAN_SESSION_FLAG = 0x02;
19
20    private const int WILL_FLAG = 0x04;
21
22    private const int WILL_QOS_SHIFT = 3;
23
24    private const int WILL_RETAIN_FLAG = 0x20;
25
26    private const int PASSWORD_FLAG = 0x40;
27
28    private const int USERNAME_FLAG = 0x80;
29
30    private const int MAX_KEEPALIVE_SECONDS = 65535;
31
32    private const int PUBLISH_RETAIN_FLAG = 0x01;
33
34    private const int PUBLISH_QOS_SHIFT = 1;
35
36    private const int SUBSCRIPTION_REQUIRED_FLAGS = 0x02;
37
38    public function encodeConnect(
39        string $clientId,
40        Duration $keepalive,
41        ?string $username = null,
42        #[SensitiveParameter]
43        ?string $password = null,
44        ?MqttMessage $will = null,
45    ): string {
46        $flags = self::CLEAN_SESSION_FLAG;
47        $payload = PacketBytes::encodeString($clientId);
48
49        if ($will !== null) {
50            $flags |= self::WILL_FLAG | ($will->qos->value << self::WILL_QOS_SHIFT) | ($will->retain ? self::WILL_RETAIN_FLAG : 0);
51            $payload .= PacketBytes::encodeString($will->topic) . PacketBytes::encodeString($will->payload);
52        }
53
54        // MQTT 3.1.1 allows a password only together with a username.
55        if ($username !== null) {
56            $flags |= self::USERNAME_FLAG;
57            $payload .= PacketBytes::encodeString($username);
58
59            if ($password !== null) {
60                $flags |= self::PASSWORD_FLAG;
61                $payload .= PacketBytes::encodeString($password);
62            }
63        }
64
65        $variableHeader = PacketBytes::encodeString(self::PROTOCOL_NAME)
66            . \chr(self::PROTOCOL_LEVEL)
67            . \chr($flags)
68            . PacketBytes::encodeUint16(min(self::MAX_KEEPALIVE_SECONDS, (int) ceil($keepalive->toSeconds())));
69
70        return PacketBytes::assemblePacket(PacketType::Connect, 0, $variableHeader . $payload);
71    }
72
73    public function encodePublish(MqttMessage $message, ?int $packetId = null): string
74    {
75        $flags = ($message->qos->value << self::PUBLISH_QOS_SHIFT) | ($message->retain ? self::PUBLISH_RETAIN_FLAG : 0);
76        $body = PacketBytes::encodeString($message->topic);
77
78        if ($message->qos !== MqttQos::AtMostOnce) {
79            $body .= PacketBytes::encodeUint16($packetId ?? 0);
80        }
81
82        return PacketBytes::assemblePacket(PacketType::Publish, $flags, $body . $message->payload);
83    }
84
85    public function encodePubAck(int $packetId): string
86    {
87        return PacketBytes::assemblePacket(PacketType::PubAck, 0, PacketBytes::encodeUint16($packetId));
88    }
89
90    public function encodeSubscribe(int $packetId, string $topicFilter, MqttQos $maximumQos): string
91    {
92        $body = PacketBytes::encodeUint16($packetId) . PacketBytes::encodeString($topicFilter) . \chr($maximumQos->value);
93
94        return PacketBytes::assemblePacket(PacketType::Subscribe, self::SUBSCRIPTION_REQUIRED_FLAGS, $body);
95    }
96
97    public function encodeUnsubscribe(int $packetId, string $topicFilter): string
98    {
99        $body = PacketBytes::encodeUint16($packetId) . PacketBytes::encodeString($topicFilter);
100
101        return PacketBytes::assemblePacket(PacketType::Unsubscribe, self::SUBSCRIPTION_REQUIRED_FLAGS, $body);
102    }
103
104    public function encodePingRequest(): string
105    {
106        return PacketBytes::assemblePacket(PacketType::PingReq, 0, '');
107    }
108
109    public function encodeDisconnect(): string
110    {
111        return PacketBytes::assemblePacket(PacketType::Disconnect, 0, '');
112    }
113}