@annadata/capacitor-mqtt-quic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +399 -0
  2. package/android/NGTCP2_BUILD_INSTRUCTIONS.md +319 -0
  3. package/android/build-nghttp3.sh +182 -0
  4. package/android/build-ngtcp2.sh +289 -0
  5. package/android/build-openssl.sh +302 -0
  6. package/android/build.gradle +75 -0
  7. package/android/gradle.properties +3 -0
  8. package/android/proguard-rules.pro +2 -0
  9. package/android/settings.gradle +1 -0
  10. package/android/src/main/AndroidManifest.xml +1 -0
  11. package/android/src/main/assets/mqttquic_ca.pem +5 -0
  12. package/android/src/main/cpp/CMakeLists.txt +157 -0
  13. package/android/src/main/cpp/ngtcp2_jni.cpp +928 -0
  14. package/android/src/main/kotlin/ai/annadata/mqttquic/MqttQuicPlugin.kt +232 -0
  15. package/android/src/main/kotlin/ai/annadata/mqttquic/client/MQTTClient.kt +339 -0
  16. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5Properties.kt +250 -0
  17. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5Protocol.kt +281 -0
  18. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTT5ReasonCodes.kt +109 -0
  19. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocol.kt +249 -0
  20. package/android/src/main/kotlin/ai/annadata/mqttquic/mqtt/MQTTTypes.kt +47 -0
  21. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/NGTCP2Client.kt +184 -0
  22. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicClientStub.kt +54 -0
  23. package/android/src/main/kotlin/ai/annadata/mqttquic/quic/QuicTypes.kt +21 -0
  24. package/android/src/main/kotlin/ai/annadata/mqttquic/transport/QUICStreamAdapter.kt +37 -0
  25. package/android/src/main/kotlin/ai/annadata/mqttquic/transport/StreamTransport.kt +91 -0
  26. package/android/src/test/kotlin/ai/annadata/mqttquic/mqtt/MQTTProtocolTest.kt +92 -0
  27. package/dist/esm/definitions.d.ts +66 -0
  28. package/dist/esm/definitions.d.ts.map +1 -0
  29. package/dist/esm/definitions.js +2 -0
  30. package/dist/esm/definitions.js.map +1 -0
  31. package/dist/esm/index.d.ts +5 -0
  32. package/dist/esm/index.d.ts.map +1 -0
  33. package/dist/esm/index.js +7 -0
  34. package/dist/esm/index.js.map +1 -0
  35. package/dist/esm/web.d.ts +28 -0
  36. package/dist/esm/web.d.ts.map +1 -0
  37. package/dist/esm/web.js +183 -0
  38. package/dist/esm/web.js.map +1 -0
  39. package/dist/plugin.cjs.js +217 -0
  40. package/dist/plugin.cjs.js.map +1 -0
  41. package/dist/plugin.js +215 -0
  42. package/dist/plugin.js.map +1 -0
  43. package/ios/MqttQuicPlugin.podspec +34 -0
  44. package/ios/NGTCP2_BUILD_INSTRUCTIONS.md +302 -0
  45. package/ios/Sources/MqttQuicPlugin/Client/MQTTClient.swift +343 -0
  46. package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5Properties.swift +280 -0
  47. package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5Protocol.swift +333 -0
  48. package/ios/Sources/MqttQuicPlugin/MQTT/MQTT5ReasonCodes.swift +113 -0
  49. package/ios/Sources/MqttQuicPlugin/MQTT/MQTTProtocol.swift +322 -0
  50. package/ios/Sources/MqttQuicPlugin/MQTT/MQTTTypes.swift +54 -0
  51. package/ios/Sources/MqttQuicPlugin/MqttQuicPlugin.swift +229 -0
  52. package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Bridge.h +29 -0
  53. package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Bridge.mm +865 -0
  54. package/ios/Sources/MqttQuicPlugin/QUIC/NGTCP2Client.swift +262 -0
  55. package/ios/Sources/MqttQuicPlugin/QUIC/QuicClientStub.swift +66 -0
  56. package/ios/Sources/MqttQuicPlugin/QUIC/QuicTypes.swift +23 -0
  57. package/ios/Sources/MqttQuicPlugin/Resources/mqttquic_ca.pem +5 -0
  58. package/ios/Sources/MqttQuicPlugin/Transport/QUICStreamAdapter.swift +50 -0
  59. package/ios/Sources/MqttQuicPlugin/Transport/StreamTransport.swift +105 -0
  60. package/ios/Tests/MQTTProtocolTests.swift +82 -0
  61. package/ios/build-nghttp3.sh +173 -0
  62. package/ios/build-ngtcp2.sh +278 -0
  63. package/ios/build-openssl.sh +405 -0
  64. package/package.json +63 -0
@@ -0,0 +1,250 @@
1
+ package ai.annadata.mqttquic.mqtt
2
+
3
+ import java.nio.charset.StandardCharsets
4
+
5
+ /**
6
+ * MQTT 5.0 Property Types. Matches MQTTD mqttd/properties.py.
7
+ */
8
+ object MQTT5PropertyType {
9
+ const val PAYLOAD_FORMAT_INDICATOR: Byte = 0x01
10
+ const val MESSAGE_EXPIRY_INTERVAL: Byte = 0x02
11
+ const val CONTENT_TYPE: Byte = 0x03
12
+ const val RESPONSE_TOPIC: Byte = 0x08
13
+ const val CORRELATION_DATA: Byte = 0x09
14
+ const val SUBSCRIPTION_IDENTIFIER: Byte = 0x0B
15
+ const val SESSION_EXPIRY_INTERVAL: Byte = 0x11
16
+ const val ASSIGNED_CLIENT_IDENTIFIER: Byte = 0x12
17
+ const val SERVER_KEEP_ALIVE: Byte = 0x13
18
+ const val AUTHENTICATION_METHOD: Byte = 0x15
19
+ const val AUTHENTICATION_DATA: Byte = 0x16
20
+ const val REQUEST_PROBLEM_INFORMATION: Byte = 0x17
21
+ const val WILL_DELAY_INTERVAL: Byte = 0x18
22
+ const val REQUEST_RESPONSE_INFORMATION: Byte = 0x19
23
+ const val RESPONSE_INFORMATION: Byte = 0x1A
24
+ const val SERVER_REFERENCE: Byte = 0x1C
25
+ const val REASON_STRING: Byte = 0x1F
26
+ const val RECEIVE_MAXIMUM: Byte = 0x21
27
+ const val TOPIC_ALIAS_MAXIMUM: Byte = 0x22
28
+ const val TOPIC_ALIAS: Byte = 0x23
29
+ const val MAXIMUM_QOS: Byte = 0x24
30
+ const val RETAIN_AVAILABLE: Byte = 0x25
31
+ const val USER_PROPERTY: Byte = 0x26
32
+ const val MAXIMUM_PACKET_SIZE: Byte = 0x27
33
+ const val WILDCARD_SUBSCRIPTION_AVAILABLE: Byte = 0x28
34
+ const val SUBSCRIPTION_IDENTIFIER_AVAILABLE: Byte = 0x29
35
+ const val SHARED_SUBSCRIPTION_AVAILABLE: Byte = 0x2A
36
+ }
37
+
38
+ /**
39
+ * MQTT 5.0 Properties encoder/decoder. Matches MQTTD mqttd/properties.py.
40
+ */
41
+ object MQTT5PropertyEncoder {
42
+
43
+ fun encodeProperties(props: Map<Int, Any>): ByteArray {
44
+ val result = mutableListOf<Byte>()
45
+ val sorted = props.toList().sortedBy { it.first }
46
+
47
+ for ((propId, value) in sorted) {
48
+ // Handle subscription identifier list
49
+ if (propId == MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER.toInt() && value is List<*>) {
50
+ for (subId in value) {
51
+ result.add(propId.toByte())
52
+ result.addAll(encodeVariableByteInteger((subId as? Int) ?: 0).toList())
53
+ }
54
+ continue
55
+ }
56
+
57
+ result.add(propId.toByte())
58
+
59
+ when (propId) {
60
+ MQTT5PropertyType.PAYLOAD_FORMAT_INDICATOR.toInt() -> {
61
+ result.add(((value as? Int) ?: 0).toByte())
62
+ }
63
+ MQTT5PropertyType.MESSAGE_EXPIRY_INTERVAL.toInt(),
64
+ MQTT5PropertyType.SESSION_EXPIRY_INTERVAL.toInt(),
65
+ MQTT5PropertyType.WILL_DELAY_INTERVAL.toInt(),
66
+ MQTT5PropertyType.MAXIMUM_PACKET_SIZE.toInt() -> {
67
+ val v = ((value as? Long) ?: 0L).toInt()
68
+ result.add((v shr 24).toByte())
69
+ result.add((v shr 16).toByte())
70
+ result.add((v shr 8).toByte())
71
+ result.add(v.toByte())
72
+ }
73
+ MQTT5PropertyType.CONTENT_TYPE.toInt(),
74
+ MQTT5PropertyType.RESPONSE_TOPIC.toInt(),
75
+ MQTT5PropertyType.ASSIGNED_CLIENT_IDENTIFIER.toInt(),
76
+ MQTT5PropertyType.AUTHENTICATION_METHOD.toInt(),
77
+ MQTT5PropertyType.RESPONSE_INFORMATION.toInt(),
78
+ MQTT5PropertyType.SERVER_REFERENCE.toInt(),
79
+ MQTT5PropertyType.REASON_STRING.toInt() -> {
80
+ result.addAll(encodeString((value as? String) ?: "").toList())
81
+ }
82
+ MQTT5PropertyType.CORRELATION_DATA.toInt(),
83
+ MQTT5PropertyType.AUTHENTICATION_DATA.toInt() -> {
84
+ val data = (value as? ByteArray) ?: ByteArray(0)
85
+ result.add((data.size shr 8).toByte())
86
+ result.add((data.size and 0xFF).toByte())
87
+ result.addAll(data.toList())
88
+ }
89
+ MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER.toInt() -> {
90
+ result.addAll(encodeVariableByteInteger((value as? Int) ?: 0).toList())
91
+ }
92
+ MQTT5PropertyType.SERVER_KEEP_ALIVE.toInt(),
93
+ MQTT5PropertyType.RECEIVE_MAXIMUM.toInt(),
94
+ MQTT5PropertyType.TOPIC_ALIAS_MAXIMUM.toInt(),
95
+ MQTT5PropertyType.TOPIC_ALIAS.toInt() -> {
96
+ val v = ((value as? Int) ?: 0).toInt()
97
+ result.add((v shr 8).toByte())
98
+ result.add((v and 0xFF).toByte())
99
+ }
100
+ MQTT5PropertyType.MAXIMUM_QOS.toInt(),
101
+ MQTT5PropertyType.RETAIN_AVAILABLE.toInt(),
102
+ MQTT5PropertyType.REQUEST_PROBLEM_INFORMATION.toInt(),
103
+ MQTT5PropertyType.REQUEST_RESPONSE_INFORMATION.toInt(),
104
+ MQTT5PropertyType.WILDCARD_SUBSCRIPTION_AVAILABLE.toInt(),
105
+ MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER_AVAILABLE.toInt(),
106
+ MQTT5PropertyType.SHARED_SUBSCRIPTION_AVAILABLE.toInt() -> {
107
+ result.add(((value as? Int) ?: 0).toByte())
108
+ }
109
+ MQTT5PropertyType.USER_PROPERTY.toInt() -> {
110
+ if (value is Pair<*, *>) {
111
+ result.addAll(encodeString((value.first as? String) ?: "").toList())
112
+ result.addAll(encodeString((value.second as? String) ?: "").toList())
113
+ } else {
114
+ throw IllegalArgumentException("USER_PROPERTY must be Pair<String, String>")
115
+ }
116
+ }
117
+ else -> throw IllegalArgumentException("Unknown property type: $propId")
118
+ }
119
+ }
120
+
121
+ return result.toByteArray()
122
+ }
123
+
124
+ fun decodeProperties(data: ByteArray, offset: Int = 0): Pair<Map<Int, Any>, Int> {
125
+ val props = mutableMapOf<Int, Any>()
126
+ var pos = offset
127
+
128
+ while (pos < data.size) {
129
+ val propId = data[pos].toInt() and 0xFF
130
+ pos++
131
+
132
+ when (propId) {
133
+ MQTT5PropertyType.PAYLOAD_FORMAT_INDICATOR.toInt() -> {
134
+ props[propId] = data[pos].toInt() and 0xFF
135
+ pos++
136
+ }
137
+ MQTT5PropertyType.MESSAGE_EXPIRY_INTERVAL.toInt(),
138
+ MQTT5PropertyType.SESSION_EXPIRY_INTERVAL.toInt(),
139
+ MQTT5PropertyType.WILL_DELAY_INTERVAL.toInt(),
140
+ MQTT5PropertyType.MAXIMUM_PACKET_SIZE.toInt() -> {
141
+ val v = ((data[pos].toInt() and 0xFF) shl 24) or
142
+ ((data[pos + 1].toInt() and 0xFF) shl 16) or
143
+ ((data[pos + 2].toInt() and 0xFF) shl 8) or
144
+ (data[pos + 3].toInt() and 0xFF)
145
+ props[propId] = v
146
+ pos += 4
147
+ }
148
+ MQTT5PropertyType.CONTENT_TYPE.toInt(),
149
+ MQTT5PropertyType.RESPONSE_TOPIC.toInt(),
150
+ MQTT5PropertyType.ASSIGNED_CLIENT_IDENTIFIER.toInt(),
151
+ MQTT5PropertyType.AUTHENTICATION_METHOD.toInt(),
152
+ MQTT5PropertyType.RESPONSE_INFORMATION.toInt(),
153
+ MQTT5PropertyType.SERVER_REFERENCE.toInt(),
154
+ MQTT5PropertyType.REASON_STRING.toInt() -> {
155
+ val (s, next) = decodeString(data, pos)
156
+ props[propId] = s
157
+ pos = next
158
+ }
159
+ MQTT5PropertyType.CORRELATION_DATA.toInt(),
160
+ MQTT5PropertyType.AUTHENTICATION_DATA.toInt() -> {
161
+ val len = ((data[pos].toInt() and 0xFF) shl 8) or (data[pos + 1].toInt() and 0xFF)
162
+ pos += 2
163
+ props[propId] = data.copyOfRange(pos, pos + len)
164
+ pos += len
165
+ }
166
+ MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER.toInt() -> {
167
+ val (v, consumed) = decodeVariableByteInteger(data, pos)
168
+ val list = (props[propId] as? MutableList<Int>) ?: mutableListOf()
169
+ list.add(v)
170
+ props[propId] = list
171
+ pos += consumed
172
+ }
173
+ MQTT5PropertyType.SERVER_KEEP_ALIVE.toInt(),
174
+ MQTT5PropertyType.RECEIVE_MAXIMUM.toInt(),
175
+ MQTT5PropertyType.TOPIC_ALIAS_MAXIMUM.toInt(),
176
+ MQTT5PropertyType.TOPIC_ALIAS.toInt() -> {
177
+ val v = ((data[pos].toInt() and 0xFF) shl 8) or (data[pos + 1].toInt() and 0xFF)
178
+ props[propId] = v
179
+ pos += 2
180
+ }
181
+ MQTT5PropertyType.MAXIMUM_QOS.toInt(),
182
+ MQTT5PropertyType.RETAIN_AVAILABLE.toInt(),
183
+ MQTT5PropertyType.REQUEST_PROBLEM_INFORMATION.toInt(),
184
+ MQTT5PropertyType.REQUEST_RESPONSE_INFORMATION.toInt(),
185
+ MQTT5PropertyType.WILDCARD_SUBSCRIPTION_AVAILABLE.toInt(),
186
+ MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER_AVAILABLE.toInt(),
187
+ MQTT5PropertyType.SHARED_SUBSCRIPTION_AVAILABLE.toInt() -> {
188
+ props[propId] = data[pos].toInt() and 0xFF
189
+ pos++
190
+ }
191
+ MQTT5PropertyType.USER_PROPERTY.toInt() -> {
192
+ val (name, next1) = decodeString(data, pos)
193
+ pos = next1
194
+ val (value, next2) = decodeString(data, pos)
195
+ pos = next2
196
+ val list = (props[propId] as? MutableList<Pair<String, String>>) ?: mutableListOf()
197
+ list.add(name to value)
198
+ props[propId] = list
199
+ }
200
+ else -> break // Unknown property - skip
201
+ }
202
+ }
203
+
204
+ return props to (pos - offset)
205
+ }
206
+
207
+ private fun encodeString(s: String): ByteArray {
208
+ val utf8 = s.toByteArray(StandardCharsets.UTF_8)
209
+ if (utf8.size > 0xFFFF) throw IllegalArgumentException("String too long")
210
+ return byteArrayOf((utf8.size shr 8).toByte(), (utf8.size and 0xFF).toByte()) + utf8
211
+ }
212
+
213
+ private fun decodeString(data: ByteArray, offset: Int): Pair<String, Int> {
214
+ if (offset + 2 > data.size) throw IllegalArgumentException("Insufficient data for string length")
215
+ val len = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF)
216
+ val start = offset + 2
217
+ if (start + len > data.size) throw IllegalArgumentException("Insufficient data for string content")
218
+ val sub = data.copyOfRange(start, start + len)
219
+ val s = String(sub, StandardCharsets.UTF_8)
220
+ return s to (start + len)
221
+ }
222
+
223
+ private fun encodeVariableByteInteger(value: Int): ByteArray {
224
+ if (value < 0 || value > 268_435_455) throw IllegalArgumentException("Invalid variable byte integer: $value")
225
+ val enc = mutableListOf<Byte>()
226
+ var n = value
227
+ do {
228
+ var b = (n % 128).toByte()
229
+ n /= 128
230
+ if (n > 0) b = (b.toInt() or 0x80).toByte()
231
+ enc.add(b)
232
+ } while (n > 0)
233
+ return enc.toByteArray()
234
+ }
235
+
236
+ private fun decodeVariableByteInteger(data: ByteArray, offset: Int): Pair<Int, Int> {
237
+ var mul = 1
238
+ var value = 0
239
+ var i = offset
240
+ repeat(4) {
241
+ if (i >= data.size) throw IllegalArgumentException("Insufficient data for variable byte integer")
242
+ val b = data[i].toInt() and 0xFF
243
+ value += (b and 0x7F) * mul
244
+ i++
245
+ if ((b and 0x80) == 0) return@repeat
246
+ mul *= 128
247
+ }
248
+ return value to (i - offset)
249
+ }
250
+ }
@@ -0,0 +1,281 @@
1
+ package ai.annadata.mqttquic.mqtt
2
+
3
+ /**
4
+ * MQTT 5.0 Protocol implementation. Matches MQTTD mqttd/protocol_v5.py.
5
+ */
6
+ object MQTT5Protocol {
7
+
8
+ fun buildConnectV5(
9
+ clientId: String,
10
+ username: String? = null,
11
+ password: String? = null,
12
+ keepalive: Int = 60,
13
+ cleanStart: Boolean = true,
14
+ sessionExpiryInterval: Int? = null,
15
+ receiveMaximum: Int? = null,
16
+ maximumPacketSize: Int? = null,
17
+ topicAliasMaximum: Int? = null,
18
+ requestResponseInformation: Int? = null,
19
+ requestProblemInformation: Int? = null,
20
+ authenticationMethod: String? = null,
21
+ authenticationData: ByteArray? = null,
22
+ properties: Map<Int, Any>? = null
23
+ ): ByteArray {
24
+ val variableHeader = mutableListOf<Byte>()
25
+ variableHeader.addAll(MQTTProtocol.encodeString("MQTT").toList())
26
+ variableHeader.add(MQTTProtocolLevel.V5)
27
+
28
+ var flags = 0
29
+ if (cleanStart) flags = flags or 0x02
30
+ if (username != null) flags = flags or MQTTConnectFlags.USERNAME
31
+ if (password != null) flags = flags or MQTTConnectFlags.PASSWORD
32
+ variableHeader.add(flags.toByte())
33
+ variableHeader.add((keepalive shr 8).toByte())
34
+ variableHeader.add((keepalive and 0xFF).toByte())
35
+
36
+ val connectProps = mutableMapOf<Int, Any>()
37
+ sessionExpiryInterval?.let { connectProps[MQTT5PropertyType.SESSION_EXPIRY_INTERVAL.toInt()] = it }
38
+ receiveMaximum?.let { connectProps[MQTT5PropertyType.RECEIVE_MAXIMUM.toInt()] = it }
39
+ maximumPacketSize?.let { connectProps[MQTT5PropertyType.MAXIMUM_PACKET_SIZE.toInt()] = it }
40
+ topicAliasMaximum?.let { connectProps[MQTT5PropertyType.TOPIC_ALIAS_MAXIMUM.toInt()] = it }
41
+ requestResponseInformation?.let { connectProps[MQTT5PropertyType.REQUEST_RESPONSE_INFORMATION.toInt()] = it }
42
+ requestProblemInformation?.let { connectProps[MQTT5PropertyType.REQUEST_PROBLEM_INFORMATION.toInt()] = it }
43
+ authenticationMethod?.let { connectProps[MQTT5PropertyType.AUTHENTICATION_METHOD.toInt()] = it }
44
+ authenticationData?.let { connectProps[MQTT5PropertyType.AUTHENTICATION_DATA.toInt()] = it }
45
+ properties?.let { connectProps.putAll(it) }
46
+
47
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(connectProps)
48
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
49
+ variableHeader.addAll(propsLen.toList())
50
+ variableHeader.addAll(propsBytes.toList())
51
+
52
+ val payload = mutableListOf<Byte>()
53
+ payload.addAll(MQTTProtocol.encodeString(clientId).toList())
54
+ payload.add(0x00) // Will Properties length = 0
55
+ username?.let { payload.addAll(MQTTProtocol.encodeString(it).toList()) }
56
+ password?.let { payload.addAll(MQTTProtocol.encodeString(it).toList()) }
57
+
58
+ val remLen = variableHeader.size + payload.size
59
+ val fixed = mutableListOf<Byte>()
60
+ fixed.add(MQTTMessageType.CONNECT)
61
+ fixed.addAll(MQTTProtocol.encodeRemainingLength(remLen).toList())
62
+
63
+ return (fixed + variableHeader + payload).toByteArray()
64
+ }
65
+
66
+ fun buildConnackV5(
67
+ reasonCode: Int = MQTT5ReasonCode.SUCCESS,
68
+ sessionPresent: Boolean = false,
69
+ properties: Map<Int, Any>? = null
70
+ ): ByteArray {
71
+ val variableHeader = mutableListOf<Byte>()
72
+ variableHeader.add(if (sessionPresent) 0x01 else 0x00)
73
+ variableHeader.add(reasonCode.toByte())
74
+
75
+ val props = properties ?: emptyMap()
76
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
77
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
78
+ variableHeader.addAll(propsLen.toList())
79
+ variableHeader.addAll(propsBytes.toList())
80
+
81
+ val remLen = variableHeader.size
82
+ val fixed = mutableListOf<Byte>()
83
+ fixed.add(MQTTMessageType.CONNACK)
84
+ fixed.addAll(MQTTProtocol.encodeRemainingLength(remLen).toList())
85
+
86
+ return (fixed + variableHeader).toByteArray()
87
+ }
88
+
89
+ fun parseConnackV5(data: ByteArray, offset: Int = 0): Triple<Boolean, Int, Map<Int, Any>> {
90
+ if (offset + 2 > data.size) throw IllegalArgumentException("Insufficient data for CONNACK")
91
+ val sessionPresent = (data[offset].toInt() and 0x01) != 0
92
+ val reasonCode = data[offset + 1].toInt() and 0xFF
93
+ var pos = offset + 2
94
+
95
+ val (propLen, propLenBytes) = MQTTProtocol.decodeRemainingLength(data, pos)
96
+ pos += propLenBytes
97
+ val (props, _) = MQTT5PropertyEncoder.decodeProperties(data.copyOfRange(pos, pos + propLen), 0)
98
+ pos += propLen
99
+
100
+ return Triple(sessionPresent, reasonCode, props)
101
+ }
102
+
103
+ fun buildPublishV5(
104
+ topic: String,
105
+ payload: ByteArray,
106
+ packetId: Int? = null,
107
+ qos: Int = 0,
108
+ retain: Boolean = false,
109
+ properties: Map<Int, Any>? = null
110
+ ): ByteArray {
111
+ var msgType = MQTTMessageType.PUBLISH.toInt()
112
+ if (qos > 0) msgType = msgType or (qos shl 1)
113
+ if (retain) msgType = msgType or 0x01
114
+
115
+ val vh = mutableListOf<Byte>()
116
+ vh.addAll(MQTTProtocol.encodeString(topic).toList())
117
+ if (qos > 0 && packetId != null) {
118
+ vh.add((packetId shr 8).toByte())
119
+ vh.add((packetId and 0xFF).toByte())
120
+ }
121
+
122
+ val props = properties ?: emptyMap()
123
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
124
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
125
+ vh.addAll(propsLen.toList())
126
+ vh.addAll(propsBytes.toList())
127
+
128
+ val pl = (vh + payload.toList()).toByteArray()
129
+ val remLen = pl.size
130
+ return byteArrayOf(
131
+ msgType.toByte(),
132
+ *MQTTProtocol.encodeRemainingLength(remLen),
133
+ *pl
134
+ )
135
+ }
136
+
137
+ fun buildSubscribeV5(
138
+ packetId: Int,
139
+ topic: String,
140
+ qos: Int = 0,
141
+ subscriptionIdentifier: Int? = null,
142
+ properties: Map<Int, Any>? = null
143
+ ): ByteArray {
144
+ val vh = mutableListOf<Byte>()
145
+ vh.add((packetId shr 8).toByte())
146
+ vh.add((packetId and 0xFF).toByte())
147
+
148
+ val props = mutableMapOf<Int, Any>()
149
+ subscriptionIdentifier?.let { props[MQTT5PropertyType.SUBSCRIPTION_IDENTIFIER.toInt()] = it }
150
+ properties?.let { props.putAll(it) }
151
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
152
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
153
+ vh.addAll(propsLen.toList())
154
+ vh.addAll(propsBytes.toList())
155
+
156
+ val pl = MQTTProtocol.encodeString(topic) + byteArrayOf((qos and 0x03).toByte())
157
+ val rem = vh.size + pl.size
158
+ return byteArrayOf(
159
+ (MQTTMessageType.SUBSCRIBE.toInt() or 0x02).toByte(),
160
+ *MQTTProtocol.encodeRemainingLength(rem),
161
+ *vh.toByteArray(),
162
+ *pl
163
+ )
164
+ }
165
+
166
+ fun buildSubackV5(
167
+ packetId: Int,
168
+ reasonCodes: List<Int>,
169
+ properties: Map<Int, Any>? = null
170
+ ): ByteArray {
171
+ val vh = mutableListOf<Byte>()
172
+ vh.add((packetId shr 8).toByte())
173
+ vh.add((packetId and 0xFF).toByte())
174
+
175
+ val props = properties ?: emptyMap()
176
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
177
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
178
+ vh.addAll(propsLen.toList())
179
+ vh.addAll(propsBytes.toList())
180
+
181
+ val pl = reasonCodes.map { it.toByte() }.toByteArray()
182
+ val rem = vh.size + pl.size
183
+ return byteArrayOf(
184
+ MQTTMessageType.SUBACK,
185
+ *MQTTProtocol.encodeRemainingLength(rem),
186
+ *vh.toByteArray(),
187
+ *pl
188
+ )
189
+ }
190
+
191
+ fun parseSubackV5(data: ByteArray, offset: Int = 0): Triple<Int, List<Int>, Map<Int, Any>> {
192
+ if (offset + 2 > data.size) throw IllegalArgumentException("Insufficient data for SUBACK packet ID")
193
+ val pid = ((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF)
194
+ var pos = offset + 2
195
+
196
+ val (propLen, propLenBytes) = MQTTProtocol.decodeRemainingLength(data, pos)
197
+ pos += propLenBytes
198
+ val (props, _) = MQTT5PropertyEncoder.decodeProperties(data.copyOfRange(pos, pos + propLen), 0)
199
+ pos += propLen
200
+
201
+ val reasonCodes = mutableListOf<Int>()
202
+ while (pos < data.size) {
203
+ reasonCodes.add(data[pos].toInt() and 0xFF)
204
+ pos++
205
+ }
206
+
207
+ return Triple(pid, reasonCodes, props)
208
+ }
209
+
210
+ fun buildUnsubscribeV5(
211
+ packetId: Int,
212
+ topics: List<String>,
213
+ properties: Map<Int, Any>? = null
214
+ ): ByteArray {
215
+ val vh = mutableListOf<Byte>()
216
+ vh.add((packetId shr 8).toByte())
217
+ vh.add((packetId and 0xFF).toByte())
218
+
219
+ val props = properties ?: emptyMap()
220
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
221
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
222
+ vh.addAll(propsLen.toList())
223
+ vh.addAll(propsBytes.toList())
224
+
225
+ val plList = topics.flatMap { MQTTProtocol.encodeString(it).toList() }
226
+ val pl = ByteArray(plList.size) { plList[it] }
227
+ val rem = vh.size + pl.size
228
+ return byteArrayOf(
229
+ (MQTTMessageType.UNSUBSCRIBE.toInt() or 0x02).toByte(),
230
+ *MQTTProtocol.encodeRemainingLength(rem),
231
+ *vh.toByteArray(),
232
+ *pl
233
+ )
234
+ }
235
+
236
+ fun buildUnsubackV5(
237
+ packetId: Int,
238
+ reasonCodes: List<Int>? = null,
239
+ properties: Map<Int, Any>? = null
240
+ ): ByteArray {
241
+ val vh = mutableListOf<Byte>()
242
+ vh.add((packetId shr 8).toByte())
243
+ vh.add((packetId and 0xFF).toByte())
244
+
245
+ val props = properties ?: emptyMap()
246
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
247
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
248
+ vh.addAll(propsLen.toList())
249
+ vh.addAll(propsBytes.toList())
250
+
251
+ val pl = reasonCodes?.map { it.toByte() }?.toByteArray() ?: ByteArray(0)
252
+ val rem = vh.size + pl.size
253
+ return byteArrayOf(
254
+ MQTTMessageType.UNSUBACK,
255
+ *MQTTProtocol.encodeRemainingLength(rem),
256
+ *vh.toByteArray(),
257
+ *pl
258
+ )
259
+ }
260
+
261
+ fun buildDisconnectV5(
262
+ reasonCode: Int = MQTT5ReasonCode.NORMAL_DISCONNECTION_DISC,
263
+ properties: Map<Int, Any>? = null
264
+ ): ByteArray {
265
+ val vh = mutableListOf<Byte>()
266
+ vh.add(reasonCode.toByte())
267
+
268
+ val props = properties ?: emptyMap()
269
+ val propsBytes = MQTT5PropertyEncoder.encodeProperties(props)
270
+ val propsLen = MQTTProtocol.encodeRemainingLength(propsBytes.size)
271
+ vh.addAll(propsLen.toList())
272
+ vh.addAll(propsBytes.toList())
273
+
274
+ val remLen = vh.size
275
+ val fixed = mutableListOf<Byte>()
276
+ fixed.add(MQTTMessageType.DISCONNECT)
277
+ fixed.addAll(MQTTProtocol.encodeRemainingLength(remLen).toList())
278
+
279
+ return (fixed + vh).toByteArray()
280
+ }
281
+ }
@@ -0,0 +1,109 @@
1
+ package ai.annadata.mqttquic.mqtt
2
+
3
+ /**
4
+ * MQTT 5.0 Reason Codes. Matches MQTTD mqttd/reason_codes.py.
5
+ */
6
+ object MQTT5ReasonCode {
7
+ // Success
8
+ const val SUCCESS: Int = 0x00
9
+ const val NORMAL_DISCONNECTION: Int = 0x00
10
+ const val DISCONNECT_WITH_WILL_MESSAGE: Int = 0x04
11
+
12
+ // CONNACK Reason Codes
13
+ const val UNSPECIFIED_ERROR: Int = 0x80
14
+ const val MALFORMED_PACKET: Int = 0x81
15
+ const val PROTOCOL_ERROR: Int = 0x82
16
+ const val IMPLEMENTATION_SPECIFIC_ERROR: Int = 0x83
17
+ const val UNSUPPORTED_PROTOCOL_VERSION: Int = 0x84
18
+ const val CLIENT_IDENTIFIER_NOT_VALID: Int = 0x85
19
+ const val BAD_USER_NAME_OR_PASSWORD: Int = 0x86
20
+ const val NOT_AUTHORIZED: Int = 0x87
21
+ const val SERVER_UNAVAILABLE: Int = 0x88
22
+ const val SERVER_BUSY: Int = 0x89
23
+ const val BANNED: Int = 0x8A
24
+ const val BAD_AUTHENTICATION_METHOD: Int = 0x8C
25
+ const val TOPIC_NAME_INVALID: Int = 0x90
26
+ const val PACKET_TOO_LARGE: Int = 0x95
27
+ const val QUOTA_EXCEEDED: Int = 0x97
28
+ const val PAYLOAD_FORMAT_INVALID: Int = 0x99
29
+ const val RETAIN_NOT_SUPPORTED: Int = 0x9A
30
+ const val QOS_NOT_SUPPORTED: Int = 0x9B
31
+ const val USE_ANOTHER_SERVER: Int = 0x9C
32
+ const val SERVER_MOVED: Int = 0x9D
33
+ const val CONNECTION_RATE_EXCEEDED: Int = 0x9F
34
+
35
+ // PUBACK, PUBREC, PUBREL, PUBCOMP Reason Codes
36
+ const val NO_MATCHING_SUBSCRIBERS: Int = 0x10
37
+ const val UNSPECIFIED_ERROR_PUB: Int = 0x80
38
+ const val IMPLEMENTATION_SPECIFIC_ERROR_PUB: Int = 0x83
39
+ const val NOT_AUTHORIZED_PUB: Int = 0x87
40
+ const val TOPIC_NAME_INVALID_PUB: Int = 0x90
41
+ const val PACKET_IDENTIFIER_IN_USE: Int = 0x91
42
+ const val QUOTA_EXCEEDED_PUB: Int = 0x97
43
+ const val PAYLOAD_FORMAT_INVALID_PUB: Int = 0x99
44
+
45
+ // SUBACK Reason Codes
46
+ const val GRANTED_QOS_0: Int = 0x00
47
+ const val GRANTED_QOS_1: Int = 0x01
48
+ const val GRANTED_QOS_2: Int = 0x02
49
+ const val UNSPECIFIED_ERROR_SUB: Int = 0x80
50
+ const val IMPLEMENTATION_SPECIFIC_ERROR_SUB: Int = 0x83
51
+ const val NOT_AUTHORIZED_SUB: Int = 0x87
52
+ const val TOPIC_FILTER_INVALID: Int = 0x8F
53
+ const val PACKET_IDENTIFIER_IN_USE_SUB: Int = 0x91
54
+ const val QUOTA_EXCEEDED_SUB: Int = 0x97
55
+ const val SHARED_SUBSCRIPTIONS_NOT_SUPPORTED: Int = 0x9E
56
+ const val SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED: Int = 0xA1
57
+ const val WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED: Int = 0xA2
58
+
59
+ // UNSUBACK Reason Codes
60
+ const val SUCCESS_UNSUB: Int = 0x00
61
+ const val NO_SUBSCRIPTION_EXISTED: Int = 0x11
62
+ const val UNSPECIFIED_ERROR_UNSUB: Int = 0x80
63
+ const val IMPLEMENTATION_SPECIFIC_ERROR_UNSUB: Int = 0x83
64
+ const val NOT_AUTHORIZED_UNSUB: Int = 0x87
65
+ const val TOPIC_FILTER_INVALID_UNSUB: Int = 0x8F
66
+ const val PACKET_IDENTIFIER_IN_USE_UNSUB: Int = 0x91
67
+
68
+ // DISCONNECT Reason Codes
69
+ const val NORMAL_DISCONNECTION_DISC: Int = 0x00
70
+ const val DISCONNECT_WITH_WILL_MESSAGE_DISC: Int = 0x04
71
+ const val UNSPECIFIED_ERROR_DISC: Int = 0x80
72
+ const val MALFORMED_PACKET_DISC: Int = 0x81
73
+ const val PROTOCOL_ERROR_DISC: Int = 0x82
74
+ const val IMPLEMENTATION_SPECIFIC_ERROR_DISC: Int = 0x83
75
+ const val NOT_AUTHORIZED_DISC: Int = 0x87
76
+ const val SERVER_BUSY_DISC: Int = 0x89
77
+ const val SERVER_SHUTTING_DOWN: Int = 0x8B
78
+ const val BAD_AUTHENTICATION_METHOD_DISC: Int = 0x8C
79
+ const val KEEP_ALIVE_TIMEOUT: Int = 0x8D
80
+ const val SESSION_TAKEN_OVER: Int = 0x8E
81
+ const val TOPIC_FILTER_INVALID_DISC: Int = 0x8F
82
+ const val TOPIC_NAME_INVALID_DISC: Int = 0x90
83
+ const val RECEIVE_MAXIMUM_EXCEEDED: Int = 0x93
84
+ const val TOPIC_ALIAS_INVALID: Int = 0x94
85
+ const val PACKET_TOO_LARGE_DISC: Int = 0x95
86
+ const val MESSAGE_RATE_TOO_HIGH: Int = 0x96
87
+ const val QUOTA_EXCEEDED_DISC: Int = 0x97
88
+ const val ADMINISTRATIVE_ACTION: Int = 0x98
89
+ const val PAYLOAD_FORMAT_INVALID_DISC: Int = 0x99
90
+ const val RETAIN_NOT_SUPPORTED_DISC: Int = 0x9A
91
+ const val QOS_NOT_SUPPORTED_DISC: Int = 0x9B
92
+ const val USE_ANOTHER_SERVER_DISC: Int = 0x9C
93
+ const val SERVER_MOVED_DISC: Int = 0x9D
94
+ const val SHARED_SUBSCRIPTIONS_NOT_SUPPORTED_DISC: Int = 0x9E
95
+ const val CONNECTION_RATE_EXCEEDED_DISC: Int = 0x9F
96
+ const val MAXIMUM_CONNECT_TIME: Int = 0xA0
97
+ const val SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED_DISC: Int = 0xA1
98
+ const val WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED_DISC: Int = 0xA2
99
+ }
100
+
101
+ // Compatibility mapping for MQTT 3.1.1 return codes
102
+ val MQTT3_TO_MQTT5_REASON_CODE = mapOf(
103
+ 0x00 to MQTT5ReasonCode.SUCCESS,
104
+ 0x01 to MQTT5ReasonCode.UNSUPPORTED_PROTOCOL_VERSION,
105
+ 0x02 to MQTT5ReasonCode.CLIENT_IDENTIFIER_NOT_VALID,
106
+ 0x03 to MQTT5ReasonCode.SERVER_UNAVAILABLE,
107
+ 0x04 to MQTT5ReasonCode.BAD_USER_NAME_OR_PASSWORD,
108
+ 0x05 to MQTT5ReasonCode.NOT_AUTHORIZED,
109
+ )