@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,262 @@
1
+ //
2
+ // NGTCP2Client.swift
3
+ // MqttQuicPlugin
4
+ //
5
+ // ngtcp2-based QUIC client implementation.
6
+ // Replaces QuicClientStub when ngtcp2 is built and linked.
7
+ //
8
+ // Build Requirements:
9
+ // - ngtcp2 static library (libngtcp2.a)
10
+ // - nghttp3 static library (libnghttp3.a)
11
+ // - OpenSSL 3.0+ or BoringSSL for TLS 1.3
12
+ // - iOS 15.0+ (for Network framework)
13
+ //
14
+
15
+ import Foundation
16
+
17
+ /// ngtcp2-based QUIC client implementation
18
+ public final class NGTCP2Client: QuicClientProtocol {
19
+
20
+ // MARK: - Properties
21
+
22
+ /// Native ngtcp2 client handle
23
+ fileprivate var clientHandle: UnsafeMutableRawPointer?
24
+
25
+ /// Connection state
26
+ private var isConnected: Bool = false
27
+
28
+ /// Host and port for connection
29
+ private var host: String?
30
+ private var port: UInt16?
31
+
32
+ /// Active streams
33
+ private var streams: [UInt64: NGTCP2Stream] = [:]
34
+ private let streamLock = NSLock()
35
+
36
+ // MARK: - Initialization
37
+
38
+ public init() {
39
+ clientHandle = ngtcp2_client_create()
40
+ }
41
+
42
+ deinit {
43
+ // Cleanup
44
+ Task {
45
+ try? await close()
46
+ }
47
+ }
48
+
49
+ // MARK: - QuicClientProtocol Implementation
50
+
51
+ public func connect(host: String, port: UInt16) async throws {
52
+ guard !isConnected else {
53
+ throw NGTCP2Error.alreadyConnected
54
+ }
55
+
56
+ self.host = host
57
+ self.port = port
58
+
59
+ guard let handle = clientHandle else {
60
+ throw NGTCP2Error.quicError("native handle not initialized")
61
+ }
62
+
63
+ let alpn = "mqtt"
64
+ let rv = host.withCString { hostPtr in
65
+ alpn.withCString { alpnPtr in
66
+ ngtcp2_client_connect(handle, hostPtr, port, alpnPtr)
67
+ }
68
+ }
69
+ if rv != 0 {
70
+ throw NGTCP2Error.quicError(lastErrorMessage())
71
+ }
72
+
73
+ isConnected = true
74
+ }
75
+
76
+ public func openStream() async throws -> QuicStreamProtocol {
77
+ guard isConnected else {
78
+ throw NGTCP2Error.notConnected
79
+ }
80
+
81
+ streamLock.lock()
82
+ defer { streamLock.unlock() }
83
+
84
+ guard let handle = clientHandle else {
85
+ throw NGTCP2Error.clientDisconnected
86
+ }
87
+
88
+ let streamId = ngtcp2_client_open_stream(handle)
89
+ if streamId < 0 {
90
+ throw NGTCP2Error.quicError(lastErrorMessage())
91
+ }
92
+
93
+ let stream = NGTCP2Stream(streamId: UInt64(streamId), client: self)
94
+ streams[UInt64(streamId)] = stream
95
+
96
+ return stream
97
+ }
98
+
99
+ public func close() async throws {
100
+ if !isConnected {
101
+ if let handle = clientHandle {
102
+ _ = ngtcp2_client_close(handle)
103
+ ngtcp2_client_destroy(handle)
104
+ clientHandle = nil
105
+ }
106
+ return
107
+ }
108
+
109
+ // Close all streams
110
+ streamLock.lock()
111
+ let activeStreams = Array(streams.values)
112
+ streams.removeAll()
113
+ streamLock.unlock()
114
+
115
+ for stream in activeStreams {
116
+ try? await stream.close()
117
+ }
118
+
119
+ if let handle = clientHandle {
120
+ _ = ngtcp2_client_close(handle)
121
+ ngtcp2_client_destroy(handle)
122
+ clientHandle = nil
123
+ }
124
+
125
+ isConnected = false
126
+ }
127
+
128
+ // MARK: - Internal Methods (for NGTCP2Stream)
129
+
130
+ /// Send data on a stream (called by NGTCP2Stream)
131
+ internal func sendStreamData(streamId: UInt64, data: Data) async throws {
132
+ guard isConnected else {
133
+ throw NGTCP2Error.notConnected
134
+ }
135
+
136
+ guard let handle = clientHandle else {
137
+ throw NGTCP2Error.clientDisconnected
138
+ }
139
+
140
+ let result = data.withUnsafeBytes { buffer in
141
+ ngtcp2_client_write_stream(handle,
142
+ Int64(streamId),
143
+ buffer.bindMemory(to: UInt8.self).baseAddress,
144
+ data.count,
145
+ 0)
146
+ }
147
+ if result != 0 {
148
+ throw NGTCP2Error.quicError(lastErrorMessage())
149
+ }
150
+ }
151
+
152
+ /// Receive data from a stream (called by packet receive handler)
153
+ internal func receiveStreamData(streamId: UInt64, data: Data) {
154
+ // Native layer delivers stream data directly; no-op here.
155
+ _ = data
156
+ }
157
+
158
+ private func lastErrorMessage() -> String {
159
+ guard let handle = clientHandle, let cStr = ngtcp2_client_last_error(handle) else {
160
+ return "unknown QUIC error"
161
+ }
162
+ return String(cString: cStr)
163
+ }
164
+ }
165
+
166
+ // MARK: - NGTCP2Stream Implementation
167
+
168
+ /// ngtcp2-based QUIC stream
169
+ internal final class NGTCP2Stream: QuicStreamProtocol {
170
+ let streamId: UInt64
171
+ private weak var client: NGTCP2Client?
172
+ private var isClosed: Bool = false
173
+
174
+ init(streamId: UInt64, client: NGTCP2Client) {
175
+ self.streamId = streamId
176
+ self.client = client
177
+ }
178
+
179
+ func read(maxBytes: Int) async throws -> Data {
180
+ guard !isClosed else {
181
+ throw NGTCP2Error.streamClosed
182
+ }
183
+ guard let client = client else {
184
+ throw NGTCP2Error.clientDisconnected
185
+ }
186
+ guard let handle = client.clientHandle else {
187
+ throw NGTCP2Error.clientDisconnected
188
+ }
189
+
190
+ while !isClosed {
191
+ var buffer = [UInt8](repeating: 0, count: maxBytes)
192
+ let nread = buffer.withUnsafeMutableBytes { rawBuffer -> Int in
193
+ let ptr = rawBuffer.bindMemory(to: UInt8.self).baseAddress
194
+ return ngtcp2_client_read_stream(handle, Int64(streamId), ptr, maxBytes)
195
+ }
196
+ if nread < 0 {
197
+ throw NGTCP2Error.quicError(client.lastErrorMessage())
198
+ }
199
+ if nread > 0 {
200
+ return Data(buffer.prefix(Int(nread)))
201
+ }
202
+ try await Task.sleep(nanoseconds: 5_000_000) // 5ms backoff
203
+ }
204
+ return Data()
205
+ }
206
+
207
+ func write(_ data: Data) async throws {
208
+ guard !isClosed else {
209
+ throw NGTCP2Error.streamClosed
210
+ }
211
+
212
+ guard let client = client else {
213
+ throw NGTCP2Error.clientDisconnected
214
+ }
215
+
216
+ try await client.sendStreamData(streamId: streamId, data: data)
217
+ }
218
+
219
+ func close() async throws {
220
+ isClosed = true
221
+
222
+ if let client = client, let handle = client.clientHandle {
223
+ _ = ngtcp2_client_close_stream(handle, Int64(streamId))
224
+ }
225
+ }
226
+
227
+ /// Called by client when data is received for this stream
228
+ func receiveData(_ data: Data) {
229
+ _ = data
230
+ }
231
+ }
232
+
233
+ // MARK: - Error Types
234
+
235
+ public enum NGTCP2Error: Error, LocalizedError {
236
+ case notConnected
237
+ case alreadyConnected
238
+ case connectionCancelled
239
+ case streamClosed
240
+ case clientDisconnected
241
+ case tlsError(String)
242
+ case quicError(String)
243
+
244
+ public var errorDescription: String? {
245
+ switch self {
246
+ case .notConnected:
247
+ return "QUIC client is not connected"
248
+ case .alreadyConnected:
249
+ return "QUIC client is already connected"
250
+ case .connectionCancelled:
251
+ return "QUIC connection was cancelled"
252
+ case .streamClosed:
253
+ return "QUIC stream is closed"
254
+ case .clientDisconnected:
255
+ return "QUIC client is disconnected"
256
+ case .tlsError(let message):
257
+ return "TLS error: \(message)"
258
+ case .quicError(let message):
259
+ return "QUIC error: \(message)"
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,66 @@
1
+ //
2
+ // QuicClientStub.swift
3
+ // MqttQuicPlugin
4
+ //
5
+ // Stub QUIC client for Phase 2. Replace with ngtcp2-backed implementation when
6
+ // ngtcp2 is built for iOS (see README). Uses in-memory stream for testing.
7
+ //
8
+
9
+ import Foundation
10
+
11
+ /// Stub stream: buffers read/write in memory. Used when ngtcp2 is not linked.
12
+ public final class QuicStreamStub: QuicStreamProtocol {
13
+ public let streamId: UInt64
14
+ private let buffer: MockStreamBuffer
15
+
16
+ public init(streamId: UInt64, buffer: MockStreamBuffer) {
17
+ self.streamId = streamId
18
+ self.buffer = buffer
19
+ }
20
+
21
+ public func read(maxBytes: Int) async throws -> Data {
22
+ let reader = MockStreamReader(buffer: buffer)
23
+ return try await reader.read(maxBytes: maxBytes)
24
+ }
25
+
26
+ public func write(_ data: Data) async throws {
27
+ let writer = MockStreamWriter(buffer: buffer)
28
+ try await writer.write(data)
29
+ }
30
+
31
+ public func close() async throws {
32
+ buffer.isClosed = true
33
+ }
34
+ }
35
+
36
+ /// Stub QUIC client. connect() succeeds without network; openStream() returns a stub stream.
37
+ /// Phase 2 proper: use ngtcp2_conn_client_new, TLS 1.3, NWConnection for UDP.
38
+ /// Pass initialReadData (e.g. CONNACK) to simulate server response for testing.
39
+ public final class QuicClientStub: QuicClientProtocol {
40
+ private var streamId: UInt64 = 0
41
+ private var buffer: MockStreamBuffer?
42
+ private var _stream: QuicStreamStub?
43
+ private let initialReadData: Data
44
+
45
+ public init(initialReadData: Data = Data()) {
46
+ self.initialReadData = initialReadData
47
+ }
48
+
49
+ public func connect(host: String, port: UInt16) async throws {
50
+ buffer = MockStreamBuffer(initialReadData: initialReadData)
51
+ streamId = 0
52
+ }
53
+
54
+ public func openStream() async throws -> QuicStreamProtocol {
55
+ guard let buf = buffer else { throw NSError(domain: "QuicClientStub", code: -1, userInfo: [NSLocalizedDescriptionKey: "Not connected"]) }
56
+ let s = QuicStreamStub(streamId: streamId, buffer: buf)
57
+ streamId += 1
58
+ _stream = s
59
+ return s
60
+ }
61
+
62
+ public func close() async throws {
63
+ _stream = nil
64
+ buffer = nil
65
+ }
66
+ }
@@ -0,0 +1,23 @@
1
+ //
2
+ // QuicTypes.swift
3
+ // MqttQuicPlugin
4
+ //
5
+ // QUIC transport types. Phase 2: ngtcp2 client + single bidirectional stream.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ /// Represents a single QUIC bidirectional stream (one per MQTT connection).
11
+ public protocol QuicStreamProtocol: AnyObject {
12
+ var streamId: UInt64 { get }
13
+ func read(maxBytes: Int) async throws -> Data
14
+ func write(_ data: Data) async throws
15
+ func close() async throws
16
+ }
17
+
18
+ /// QUIC client: connect, TLS handshake, open one bidirectional stream.
19
+ public protocol QuicClientProtocol: AnyObject {
20
+ func connect(host: String, port: UInt16) async throws
21
+ func openStream() async throws -> QuicStreamProtocol
22
+ func close() async throws
23
+ }
@@ -0,0 +1,5 @@
1
+ # Replace this file with your CA certificate bundle (PEM).
2
+ # Example:
3
+ # -----BEGIN CERTIFICATE-----
4
+ # ...
5
+ # -----END CERTIFICATE-----
@@ -0,0 +1,50 @@
1
+ //
2
+ // QUICStreamAdapter.swift
3
+ // MqttQuicPlugin
4
+ //
5
+ // StreamReader/StreamWriter over QUIC stream. Mirrors NGTCP2StreamReader/NGTCP2StreamWriter.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ /// MQTTStreamReader over a QuicStream.
11
+ public final class QUICStreamReader: MQTTStreamReaderProtocol {
12
+ private let stream: QuicStreamProtocol
13
+
14
+ public init(stream: QuicStreamProtocol) {
15
+ self.stream = stream
16
+ }
17
+
18
+ public func read(maxBytes: Int) async throws -> Data {
19
+ try await stream.read(maxBytes: maxBytes)
20
+ }
21
+
22
+ public func readexactly(_ n: Int) async throws -> Data {
23
+ var acc = Data()
24
+ while acc.count < n {
25
+ let chunk = try await stream.read(maxBytes: n - acc.count)
26
+ if chunk.isEmpty { throw MQTTProtocolError.insufficientData("readexactly") }
27
+ acc.append(chunk)
28
+ }
29
+ return acc
30
+ }
31
+ }
32
+
33
+ /// MQTTStreamWriter over a QuicStream.
34
+ public final class QUICStreamWriter: MQTTStreamWriterProtocol {
35
+ private let stream: QuicStreamProtocol
36
+
37
+ public init(stream: QuicStreamProtocol) {
38
+ self.stream = stream
39
+ }
40
+
41
+ public func write(_ data: Data) async throws {
42
+ try await stream.write(data)
43
+ }
44
+
45
+ public func drain() async throws {}
46
+
47
+ public func close() async throws {
48
+ try await stream.close()
49
+ }
50
+ }
@@ -0,0 +1,105 @@
1
+ //
2
+ // StreamTransport.swift
3
+ // MqttQuicPlugin
4
+ //
5
+ // Transport abstraction: StreamReader-like and StreamWriter-like.
6
+ // Phase 2 will implement these over QUIC streams; Phase 1 uses mocks for tests.
7
+ //
8
+
9
+ import Foundation
10
+
11
+ /// StreamReader-like interface: read(n), readexactly(n).
12
+ /// Async where applicable (Phase 2 uses async).
13
+ public protocol MQTTStreamReaderProtocol: AnyObject {
14
+ func read(maxBytes: Int) async throws -> Data
15
+ func readexactly(_ n: Int) async throws -> Data
16
+ }
17
+
18
+ /// StreamWriter-like interface: write(data), drain(), close().
19
+ public protocol MQTTStreamWriterProtocol: AnyObject {
20
+ func write(_ data: Data) async throws
21
+ func drain() async throws
22
+ func close() async throws
23
+ }
24
+
25
+ // MARK: - Mock implementations (Phase 1 unit tests)
26
+
27
+ /// In-memory buffer for mock read/write.
28
+ public final class MockStreamBuffer {
29
+ public private(set) var readBuffer: Data
30
+ public private(set) var writeBuffer: Data
31
+ public var isClosed: Bool = false
32
+
33
+ public init(initialReadData: Data = Data()) {
34
+ self.readBuffer = initialReadData
35
+ self.writeBuffer = Data()
36
+ }
37
+
38
+ public func appendRead(_ data: Data) {
39
+ readBuffer.append(data)
40
+ }
41
+
42
+ public func consumeRead(maxBytes: Int) -> Data {
43
+ let n = min(maxBytes, readBuffer.count)
44
+ let out = readBuffer.prefix(n)
45
+ readBuffer = readBuffer.dropFirst(n)
46
+ return Data(out)
47
+ }
48
+
49
+ public func consumeWrite() -> Data {
50
+ let d = writeBuffer
51
+ writeBuffer = Data()
52
+ return d
53
+ }
54
+ }
55
+
56
+ /// Mock reader: reads from MockStreamBuffer.
57
+ public final class MockStreamReader: MQTTStreamReaderProtocol {
58
+ private let buffer: MockStreamBuffer
59
+
60
+ public init(buffer: MockStreamBuffer) {
61
+ self.buffer = buffer
62
+ }
63
+
64
+ public func read(maxBytes: Int) async throws -> Data {
65
+ if buffer.isClosed && buffer.readBuffer.isEmpty {
66
+ return Data()
67
+ }
68
+ let n = min(maxBytes, buffer.readBuffer.count)
69
+ guard n > 0 else {
70
+ try await Task.sleep(nanoseconds: 1_000_000) // 1ms
71
+ return try await read(maxBytes: maxBytes)
72
+ }
73
+ return buffer.consumeRead(maxBytes: n)
74
+ }
75
+
76
+ public func readexactly(_ n: Int) async throws -> Data {
77
+ var acc = Data()
78
+ while acc.count < n {
79
+ if buffer.isClosed { throw MQTTProtocolError.insufficientData("stream closed") }
80
+ let chunk = try await read(maxBytes: n - acc.count)
81
+ if chunk.isEmpty { throw MQTTProtocolError.insufficientData("readexactly") }
82
+ acc.append(chunk)
83
+ }
84
+ return acc
85
+ }
86
+ }
87
+
88
+ /// Mock writer: appends to MockStreamBuffer.
89
+ public final class MockStreamWriter: MQTTStreamWriterProtocol {
90
+ private let buffer: MockStreamBuffer
91
+
92
+ public init(buffer: MockStreamBuffer) {
93
+ self.buffer = buffer
94
+ }
95
+
96
+ public func write(_ data: Data) async throws {
97
+ buffer.writeBuffer.append(data)
98
+ }
99
+
100
+ public func drain() async throws {}
101
+
102
+ public func close() async throws {
103
+ buffer.isClosed = true
104
+ }
105
+ }
@@ -0,0 +1,82 @@
1
+ //
2
+ // MQTTProtocolTests.swift
3
+ // Unit tests for MQTT encode/decode. Run via Xcode test target or Swift Package.
4
+ //
5
+
6
+ import XCTest
7
+ @testable import MqttQuicPlugin
8
+
9
+ final class MQTTProtocolTests: XCTestCase {
10
+
11
+ func testEncodeDecodeRemainingLength() throws {
12
+ for (len, bytes) in [(0, 1), (127, 1), (128, 2), (16383, 2), (16384, 3), (2_097_151, 3), (2_097_152, 4)] {
13
+ let enc = try MQTTProtocol.encodeRemainingLength(len)
14
+ XCTAssertEqual(enc.count, bytes, "length \(len)")
15
+ let (dec, _) = try MQTTProtocol.decodeRemainingLength(Data(enc), offset: 0)
16
+ XCTAssertEqual(dec, len)
17
+ }
18
+ }
19
+
20
+ func testEncodeDecodeString() throws {
21
+ let s = "hello/mqtt"
22
+ let enc = try MQTTProtocol.encodeString(s)
23
+ XCTAssertEqual(enc.count, 2 + s.utf8.count)
24
+ let (dec, _) = try MQTTProtocol.decodeString(enc, offset: 0)
25
+ XCTAssertEqual(dec, s)
26
+ }
27
+
28
+ func testBuildConnect() throws {
29
+ let data = try MQTTProtocol.buildConnect(clientId: "test-client", username: "u", password: "p", keepalive: 90, cleanSession: true)
30
+ XCTAssertGreaterThanOrEqual(data.count, 10)
31
+ XCTAssertEqual(data[0], MQTTMessageType.CONNECT.rawValue)
32
+ }
33
+
34
+ func testBuildConnack() {
35
+ let data = MQTTProtocol.buildConnack(returnCode: MQTTConnAckCode.accepted.rawValue)
36
+ XCTAssertEqual(data.count, 4)
37
+ XCTAssertEqual(data[0], MQTTMessageType.CONNACK.rawValue)
38
+ XCTAssertEqual(data[3], MQTTConnAckCode.accepted.rawValue)
39
+ }
40
+
41
+ func testBuildPublish() throws {
42
+ let payload = Data("hello".utf8)
43
+ let data = try MQTTProtocol.buildPublish(topic: "a/b", payload: payload, qos: 0, retain: false)
44
+ XCTAssertEqual(data[0] & 0xF0, MQTTMessageType.PUBLISH.rawValue)
45
+ }
46
+
47
+ func testBuildSubscribeAndSuback() throws {
48
+ let sub = try MQTTProtocol.buildSubscribe(packetId: 1, topic: "t/1", qos: 0)
49
+ XCTAssertEqual(sub[0], MQTTMessageType.SUBSCRIBE.rawValue | 0x02)
50
+
51
+ let suback = MQTTProtocol.buildSuback(packetId: 1, returnCode: 0)
52
+ XCTAssertEqual(suback[0], MQTTMessageType.SUBACK.rawValue)
53
+ let (pid, rc, _) = try MQTTProtocol.parseSuback(suback, offset: 2) // skip fixed header (2 bytes)
54
+ XCTAssertEqual(pid, 1)
55
+ XCTAssertEqual(rc, 0)
56
+ }
57
+
58
+ func testBuildPingreqPingrespDisconnect() {
59
+ let pr = MQTTProtocol.buildPingreq()
60
+ XCTAssertEqual(pr[0], MQTTMessageType.PINGREQ.rawValue)
61
+ let ps = MQTTProtocol.buildPingresp()
62
+ XCTAssertEqual(ps[0], MQTTMessageType.PINGRESP.rawValue)
63
+ let dc = MQTTProtocol.buildDisconnect()
64
+ XCTAssertEqual(dc[0], MQTTMessageType.DISCONNECT.rawValue)
65
+ }
66
+
67
+ func testMockStreamReaderWriter() async throws {
68
+ let buf = MockStreamBuffer(initialReadData: Data([1, 2, 3, 4, 5]))
69
+ let reader = MockStreamReader(buffer: buf)
70
+ let writer = MockStreamWriter(buffer: buf)
71
+
72
+ let r1 = try await reader.read(maxBytes: 2)
73
+ XCTAssertEqual(r1, Data([1, 2]))
74
+ let r2 = try await reader.readexactly(3)
75
+ XCTAssertEqual(r2, Data([3, 4, 5]))
76
+
77
+ try await writer.write(Data([6, 7, 8]))
78
+ try await writer.drain()
79
+ let written = buf.consumeWrite()
80
+ XCTAssertEqual(written, Data([6, 7, 8]))
81
+ }
82
+ }