cj-tauri 0.8.0 → 1.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 (124) hide show
  1. package/README.md +3 -3
  2. package/cjpm.toml +52 -25
  3. package/cli/cjpm.toml +1 -1
  4. package/cli/src/doctor.cj +304 -0
  5. package/cli/src/main.cj +33 -7
  6. package/cli/src/project.cj +59 -1
  7. package/cli/src/resolve.cj +348 -304
  8. package/cli/src/scaffold.cj +139 -22
  9. package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
  10. package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
  11. package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
  12. package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
  13. package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
  14. package/cli/templates/ohos-pc/README.md +79 -0
  15. package/cli/templates/ohos-pc/build-hap.sh +190 -0
  16. package/cli/templates/ohos-pc/build-profile.json5 +46 -0
  17. package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
  18. package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
  19. package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
  20. package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
  21. package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
  22. package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
  23. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
  24. package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
  25. package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
  26. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
  27. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
  28. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
  29. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
  30. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
  31. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
  32. package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
  33. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
  34. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
  35. package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
  36. package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
  37. package/cli/templates/ohos-pc/oh-package.json5 +10 -0
  38. package/native/build_linux.sh +30 -2
  39. package/native/build_win.bat +62 -61
  40. package/native/core/bridge_core.c +1626 -0
  41. package/native/{bridge_core.h → core/bridge_core.h} +246 -6
  42. package/native/platforms/harmonyos/README.md +106 -0
  43. package/native/platforms/linux/bridge_linux.c +2485 -0
  44. package/native/platforms/windows/bridge_win.c +3248 -0
  45. package/native/tests/test_bridge_core.c +528 -1
  46. package/package.json +12 -3
  47. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  48. package/src/api_system.cj +10 -2
  49. package/src/app.cj +248 -85
  50. package/src/arkweb_proxy.cj +56 -0
  51. package/src/arkweb_script.cj +154 -0
  52. package/src/capability.cj +140 -0
  53. package/src/clipboard_bridge.cj +55 -0
  54. package/src/dragdrop.cj +26 -0
  55. package/src/embedded_assets.cj +98 -0
  56. package/src/hexutil.cj +53 -0
  57. package/src/host.cj +197 -26
  58. package/src/host_arkweb.cj +740 -0
  59. package/src/host_factory.cj +48 -0
  60. package/src/host_webkit.cj +202 -26
  61. package/src/host_webview2.cj +176 -3
  62. package/src/hotkey.cj +66 -0
  63. package/src/ipc_hub.cj +49 -0
  64. package/src/ipc_message.cj +2 -1
  65. package/src/jsutil.cj +33 -0
  66. package/src/log_bridge.cj +107 -0
  67. package/src/logging.cj +127 -0
  68. package/src/menu.cj +197 -196
  69. package/src/plugin.cj +40 -0
  70. package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
  71. package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
  72. package/src/plugins/fs/plugin_fs.cj +194 -0
  73. package/src/plugins/http/plugin_http.cj +459 -0
  74. package/src/plugins/log/plugin_log.cj +105 -0
  75. package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
  76. package/src/plugins/mqtt/mqtt_codec.cj +661 -0
  77. package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
  78. package/src/plugins/notification/plugin_notification.cj +146 -0
  79. package/src/plugins/plugins.cj +5 -0
  80. package/src/plugins/redis/plugin_redis.cj +2409 -0
  81. package/src/plugins/redis/redis_codec.cj +556 -0
  82. package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
  83. package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
  84. package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
  85. package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
  86. package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
  87. package/src/plugins/store/plugin_store.cj +287 -0
  88. package/src/plugins/ws/plugin_ws.cj +470 -0
  89. package/src/process_hidden.cj +174 -0
  90. package/src/scope.cj +391 -0
  91. package/src/shell_event.cj +69 -68
  92. package/src/tests/capability_test.cj +53 -0
  93. package/src/tests/fake_host.cj +85 -20
  94. package/src/tests/ipc_hub_test.cj +13 -0
  95. package/src/tests/jsutil_test.cj +62 -0
  96. package/src/tests/logging_test.cj +62 -0
  97. package/src/tests/mqtt_codec_test.cj +405 -0
  98. package/src/tests/plugin_clipboard_test.cj +222 -0
  99. package/src/tests/plugin_dialog_test.cj +98 -4
  100. package/src/tests/plugin_http_test.cj +124 -0
  101. package/src/tests/plugin_menu_test.cj +1 -0
  102. package/src/tests/plugin_notification_test.cj +205 -0
  103. package/src/tests/plugin_serial_test.cj +269 -267
  104. package/src/tests/plugin_shell_test.cj +189 -188
  105. package/src/tests/plugin_sqlite_test.cj +230 -0
  106. package/src/tests/plugin_store_test.cj +260 -0
  107. package/src/tests/plugin_test.cj +542 -524
  108. package/src/tests/plugin_ws_test.cj +40 -0
  109. package/src/tests/process_hidden_test.cj +129 -0
  110. package/src/tests/redis_codec_test.cj +396 -0
  111. package/src/tests/scope_test.cj +350 -0
  112. package/src/tests/shell_event_test.cj +0 -1
  113. package/src/tests/system_host_test.cj +81 -79
  114. package/src/tests/urlutil_test.cj +53 -0
  115. package/src/tests/version_test.cj +75 -65
  116. package/src/tray.cj +90 -0
  117. package/src/urlutil.cj +159 -0
  118. package/src/version.cj +23 -2
  119. package/src/window.cj +51 -42
  120. package/native/bridge_core.c +0 -797
  121. package/native/bridge_linux.c +0 -1268
  122. package/native/bridge_win.c +0 -1633
  123. package/src/plugin_fs.cj +0 -118
  124. /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
@@ -0,0 +1,661 @@
1
+ package cjTauri.plugins.mqtt
2
+
3
+ import cjTauri.*
4
+
5
+ import std.collection.*
6
+
7
+ /**
8
+ * MQTT 3.1.1 报文的纯编解码层(只吃字节、只吐字节,不碰 socket)
9
+ *
10
+ * 为什么自己写:std / stdx 都没有 MQTT,协议本身也只是「固定头 + 变长剩余长度 + 变长头 + 载荷」
11
+ * 的字节拼装,几个纯函数足够(设计取舍见 docs/设计决策记录.md D-014)。**编解码与连接状态机
12
+ * 分开**:本文件的函数一律无副作用、可单测;socket 读写与保活放在 plugin_mqtt.cj。
13
+ *
14
+ * 边界(如实):只做 MQTT 3.1.1(协议级别 4)的明文 TCP 报文;不做 MQTT 5.0 的属性/原因码,
15
+ * 不做 MQTT over WebSocket。解码遇到「剩余长度与实际字节数对不上」「类型码非法」一律返回 None
16
+ * ——宁可拒绝一条畸形报文,也不猜它想干什么(畸形报文进到状态机里,表现只会更难查)。
17
+ */
18
+
19
+ // ---------------------------------------------------------------- 常量
20
+
21
+ // 控制报文类型(MQTT 3.1.1 §2.2.1)
22
+ public const MQTT_PKT_CONNECT: UInt8 = 1
23
+ public const MQTT_PKT_CONNACK: UInt8 = 2
24
+ public const MQTT_PKT_PUBLISH: UInt8 = 3
25
+ public const MQTT_PKT_PUBACK: UInt8 = 4
26
+ public const MQTT_PKT_PUBREC: UInt8 = 5
27
+ public const MQTT_PKT_PUBREL: UInt8 = 6
28
+ public const MQTT_PKT_PUBCOMP: UInt8 = 7
29
+ public const MQTT_PKT_SUBSCRIBE: UInt8 = 8
30
+ public const MQTT_PKT_SUBACK: UInt8 = 9
31
+ public const MQTT_PKT_UNSUBSCRIBE: UInt8 = 10
32
+ public const MQTT_PKT_UNSUBACK: UInt8 = 11
33
+ public const MQTT_PKT_PINGREQ: UInt8 = 12
34
+ public const MQTT_PKT_PINGRESP: UInt8 = 13
35
+ public const MQTT_PKT_DISCONNECT: UInt8 = 14
36
+
37
+ /** 协议名与级别:CONNECT 的变长头里写死这两个值(3.1.1 = 级别 4) */
38
+ public let MQTT_PROTOCOL_NAME: String = "MQTT"
39
+ public let MQTT_PROTOCOL_LEVEL: UInt8 = 4
40
+
41
+ /** 剩余长度是 4 个 7 位字节,协议上限 256MB-1 */
42
+ public let MQTT_MAX_REMAINING_LENGTH: Int64 = 268435455
43
+
44
+ /** 单条发布载荷上限 256 KiB:调试工具不做大文件搬运工,超了直接拒(前端也能一眼看懂) */
45
+ public let MQTT_MAX_PAYLOAD: Int64 = 262144
46
+
47
+ /** MQTT 的 UTF-8 字符串(主题/客户端标识/用户名)都以 2 字节长度打头,上限 65535 字节 */
48
+ public let MQTT_MAX_UTF8_FIELD: Int64 = 65535
49
+
50
+ // ---------------------------------------------------------------- 底层读写
51
+
52
+ /** 夹到 [lo, hi]:所有「算出来再编码」的整数都先过这里(越界转换会抛异常,AGENTS §4 有先例) */
53
+ func mqttClampInt(v: Int64, lo: Int64, hi: Int64): Int64 {
54
+ if (v < lo) {
55
+ return lo
56
+ }
57
+ if (v > hi) {
58
+ return hi
59
+ }
60
+ return v
61
+ }
62
+
63
+ /** QoS 夹到 0..2:页面传 3/负数/缺省都按最近合法值处理,不抛(协议层拒绝发生在编码处) */
64
+ public func mqttClampQos(q: Int64): Int64 {
65
+ return mqttClampInt(q, 0, 2)
66
+ }
67
+
68
+ /** 大端写一个 u16(MQTT 全程大端) */
69
+ func mqttPutU16(buf: ArrayList<UInt8>, v: Int64): Unit {
70
+ let x = mqttClampInt(v, 0, 65535)
71
+ buf.add(UInt8((x >> 8) & 0xFF))
72
+ buf.add(UInt8(x & 0xFF))
73
+ }
74
+
75
+ /** 大端读一个 u16;越界读回 -1(调用方先判 body 长度,这里只兜底) */
76
+ func mqttGetU16(buf: Array<UInt8>, at: Int64): Int64 {
77
+ if (at < 0 || at + 1 >= buf.size) {
78
+ return -1
79
+ }
80
+ return (Int64(buf[at]) << 8) | Int64(buf[at + 1])
81
+ }
82
+
83
+ /** 固定头第 1 字节:类型码(高 4 位)+ 标志(低 4 位) */
84
+ func mqttTypeFlags(typeCode: UInt8, flags: Int64): UInt8 {
85
+ return UInt8((Int64(typeCode) << 4) | (flags & 0x0F))
86
+ }
87
+
88
+ /** 把 ArrayList 的字节追加到另一个 ArrayList(组帧用,避免到处写 for 循环) */
89
+ func mqttAppend(dst: ArrayList<UInt8>, src: Array<UInt8>): Unit {
90
+ for (b in src) {
91
+ dst.add(b)
92
+ }
93
+ }
94
+
95
+ // ---------------------------------------------------------------- 剩余长度(变长整数)
96
+
97
+ /**
98
+ * 剩余长度编码:每字节 7 位有效、最高位表示「后面还有」(1..4 字节)
99
+ *
100
+ * 超范围直接抛:这类错来自调用方的长度计算,静默截断会让对端收到一条长度对不上的报文,
101
+ * 表现是「broker 无响应」——比当场报错难查得多。
102
+ */
103
+ public func mqttEncodeRemainingLength(n: Int64): Array<UInt8> {
104
+ if (n < 0 || n > MQTT_MAX_REMAINING_LENGTH) {
105
+ throw CommandException("MQTT 剩余长度超范围(${n},上限 ${MQTT_MAX_REMAINING_LENGTH})")
106
+ }
107
+ let out = ArrayList<UInt8>()
108
+ var x = n
109
+ var more = true
110
+ while (more) {
111
+ let digit = x % 128
112
+ x = x / 128
113
+ if (x > 0) {
114
+ out.add(UInt8(digit | 0x80))
115
+ } else {
116
+ out.add(UInt8(digit))
117
+ more = false
118
+ }
119
+ }
120
+ return out.toArray()
121
+ }
122
+
123
+ /** 剩余长度解码结果(用类而不是元组:单测断言写起来更直白,也省掉 Option<元组> 的匹配) */
124
+ public class MqttRemainingLength {
125
+ /** 解出来的值 */
126
+ public var value: Int64 = 0
127
+ /** 占用了几个字节(1..4) */
128
+ public var consumed: Int64 = 0
129
+ }
130
+
131
+ /**
132
+ * 从 `at` 处解码剩余长度。字节不够 → None;连续 4 个字节都带续接位 → None(协议非法)。
133
+ * 只解码、不判总长:够不够由读循环负责(它才知道 socket 上还有没有字节)。
134
+ */
135
+ public func mqttDecodeRemainingLength(buf: Array<UInt8>, at: Int64): Option<MqttRemainingLength> {
136
+ var value: Int64 = 0
137
+ var multiplier: Int64 = 1
138
+ var i: Int64 = 0
139
+ while (i < 4) {
140
+ let pos = at + i
141
+ if (pos >= buf.size) {
142
+ return None
143
+ }
144
+ let b = Int64(buf[pos])
145
+ value += (b & 0x7F) * multiplier
146
+ if ((b & 0x80) == 0) {
147
+ let rl = MqttRemainingLength()
148
+ rl.value = value
149
+ rl.consumed = i + 1
150
+ return Some(rl)
151
+ }
152
+ multiplier *= 128
153
+ i += 1
154
+ }
155
+ return None
156
+ }
157
+
158
+ // ---------------------------------------------------------------- 组帧与 UTF-8 字段
159
+
160
+ /**
161
+ * MQTT 的 UTF-8 字符串:2 字节长度 + UTF-8 字节
162
+ *
163
+ * 长度按**字节数**算(`String.toArray()` 给的就是 UTF-8 字节),中文主题因此是 3 字节/字——
164
+ * 按码点数算会导致长度字段偏小、对端解析错位(AGENTS §4 那条「迭代 String 得到的是字节」同源)。
165
+ */
166
+ public func mqttUtf8Field(s: String): Array<UInt8> {
167
+ let raw = s.toArray()
168
+ if (Int64(raw.size) > MQTT_MAX_UTF8_FIELD) {
169
+ throw CommandException("MQTT 字段太长(${raw.size} 字节,上限 ${MQTT_MAX_UTF8_FIELD})")
170
+ }
171
+ let out = ArrayList<UInt8>()
172
+ mqttPutU16(out, Int64(raw.size))
173
+ mqttAppend(out, raw)
174
+ return out.toArray()
175
+ }
176
+
177
+ /** 二进制字段(遗嘱载荷/发布载荷):同样 2 字节长度打头,内容原样 */
178
+ public func mqttBinaryField(data: Array<UInt8>): Array<UInt8> {
179
+ if (Int64(data.size) > MQTT_MAX_UTF8_FIELD) {
180
+ throw CommandException("MQTT 二进制字段太长(${data.size} 字节,上限 ${MQTT_MAX_UTF8_FIELD})")
181
+ }
182
+ let out = ArrayList<UInt8>()
183
+ mqttPutU16(out, Int64(data.size))
184
+ mqttAppend(out, data)
185
+ return out.toArray()
186
+ }
187
+
188
+ /** 组一条控制报文:类型+标志 | 剩余长度 | 变长头+载荷 */
189
+ public func mqttFrame(typeCode: UInt8, flags: Int64, body: Array<UInt8>): Array<UInt8> {
190
+ let out = ArrayList<UInt8>()
191
+ out.add(mqttTypeFlags(typeCode, flags))
192
+ mqttAppend(out, mqttEncodeRemainingLength(Int64(body.size)))
193
+ mqttAppend(out, body)
194
+ return out.toArray()
195
+ }
196
+
197
+ // ---------------------------------------------------------------- CONNECT
198
+
199
+ /** CONNECT 的可选项:编码函数只吃这个对象(单测可直接构造,不必绕 JSON 参数) */
200
+ public class MqttConnectOptions {
201
+ public var host: String = "127.0.0.1"
202
+ public var port: Int64 = 1883
203
+ public var clientId: String = ""
204
+ public var username: Option<String> = None
205
+ public var password: Option<String> = None
206
+ public var keepaliveSec: Int64 = 60
207
+ public var cleanSession: Bool = true
208
+ public var willTopic: Option<String> = None
209
+ public var willPayload: Array<UInt8> = Array<UInt8>()
210
+ public var willQos: Int64 = 0
211
+ public var willRetain: Bool = false
212
+ }
213
+
214
+ /** 客户端标识为空时的兜底 id:broker 对空 id 各家处理不同,给个稳定前缀更好对账 */
215
+ public func mqttDefaultClientId(seed: Int64): String {
216
+ return "cj-tauri-" + seed.toString()
217
+ }
218
+
219
+ /**
220
+ * 组 CONNECT:协议名/级别 → 连接标志位域 → 保活 → 客户端标识 → 遗嘱 → 用户名 → 密码
221
+ *
222
+ * 连接标志是协议定死的位域,错一位对端就断连且通常不给原因,所以这里按位拼、**不吃页面传来的
223
+ * 任何原始标志值**。密码标志只能在用户名标志为 1 时置位(§3.1.2.9),非法组合当场拒。
224
+ */
225
+ public func mqttEncodeConnect(o: MqttConnectOptions): Array<UInt8> {
226
+ var user = ""
227
+ if (let Some(u) <- o.username) {
228
+ user = u
229
+ }
230
+ var pass = ""
231
+ if (let Some(p) <- o.password) {
232
+ pass = p
233
+ }
234
+ var willTopic = ""
235
+ if (let Some(w) <- o.willTopic) {
236
+ willTopic = w
237
+ }
238
+ if (user.size == 0 && pass.size > 0) {
239
+ throw CommandException("MQTT 连接参数非法:给了 password 就必须同时给 username")
240
+ }
241
+ let hasWill = willTopic.size > 0
242
+ var flags: Int64 = 0
243
+ if (o.cleanSession) {
244
+ flags = flags | 0x02
245
+ }
246
+ if (hasWill) {
247
+ flags = flags | 0x04
248
+ flags = flags | (mqttClampQos(o.willQos) << 3)
249
+ if (o.willRetain) {
250
+ flags = flags | 0x20
251
+ }
252
+ }
253
+ if (pass.size > 0) {
254
+ flags = flags | 0x40
255
+ }
256
+ if (user.size > 0) {
257
+ flags = flags | 0x80
258
+ }
259
+
260
+ let body = ArrayList<UInt8>()
261
+ mqttAppend(body, mqttUtf8Field(MQTT_PROTOCOL_NAME))
262
+ body.add(MQTT_PROTOCOL_LEVEL)
263
+ body.add(UInt8(flags & 0xFF))
264
+ mqttPutU16(body, mqttClampInt(o.keepaliveSec, 0, 65535))
265
+ mqttAppend(body, mqttUtf8Field(o.clientId))
266
+ if (hasWill) {
267
+ mqttAppend(body, mqttUtf8Field(willTopic))
268
+ mqttAppend(body, mqttBinaryField(o.willPayload))
269
+ }
270
+ if (user.size > 0) {
271
+ mqttAppend(body, mqttUtf8Field(user))
272
+ }
273
+ if (pass.size > 0) {
274
+ mqttAppend(body, mqttUtf8Field(pass))
275
+ }
276
+ return mqttFrame(MQTT_PKT_CONNECT, 0, body.toArray())
277
+ }
278
+
279
+ // ---------------------------------------------------------------- PUBLISH / SUBSCRIBE / 确认
280
+
281
+ /** 组 PUBLISH。QoS>0 必须带非零报文标识符;载荷超上限直接拒(不切分,免得静默丢内容) */
282
+ public func mqttEncodePublish(topic: String, payload: Array<UInt8>, qos: Int64, retain: Bool, dup: Bool,
283
+ packetId: Int64): Array<UInt8> {
284
+ let q = mqttClampQos(qos)
285
+ if (Int64(payload.size) > MQTT_MAX_PAYLOAD) {
286
+ throw CommandException("MQTT 载荷太大(${payload.size} 字节,上限 ${MQTT_MAX_PAYLOAD})")
287
+ }
288
+ var flags: Int64 = q << 1
289
+ if (retain) {
290
+ flags = flags | 0x01
291
+ }
292
+ if (dup) {
293
+ flags = flags | 0x08
294
+ }
295
+ let body = ArrayList<UInt8>()
296
+ mqttAppend(body, mqttUtf8Field(topic))
297
+ if (q > 0) {
298
+ if (packetId <= 0) {
299
+ throw CommandException("MQTT QoS ${q} 的发布缺少报文标识符")
300
+ }
301
+ mqttPutU16(body, packetId)
302
+ }
303
+ mqttAppend(body, payload)
304
+ return mqttFrame(MQTT_PKT_PUBLISH, flags, body.toArray())
305
+ }
306
+
307
+ /** 组 SUBSCRIBE(一次一个过滤器:调试工具逐条订阅,回执与请求一一对应更好对账) */
308
+ public func mqttEncodeSubscribe(packetId: Int64, topicFilter: String, qos: Int64): Array<UInt8> {
309
+ if (packetId <= 0) {
310
+ throw CommandException("MQTT 订阅缺少报文标识符")
311
+ }
312
+ let body = ArrayList<UInt8>()
313
+ mqttPutU16(body, packetId)
314
+ mqttAppend(body, mqttUtf8Field(topicFilter))
315
+ body.add(UInt8(mqttClampQos(qos)))
316
+ return mqttFrame(MQTT_PKT_SUBSCRIBE, 0x02, body.toArray()) // 标志位协议固定 0b0010
317
+ }
318
+
319
+ /** 组 UNSUBSCRIBE(同样一次一个过滤器) */
320
+ public func mqttEncodeUnsubscribe(packetId: Int64, topicFilter: String): Array<UInt8> {
321
+ if (packetId <= 0) {
322
+ throw CommandException("MQTT 退订缺少报文标识符")
323
+ }
324
+ let body = ArrayList<UInt8>()
325
+ mqttPutU16(body, packetId)
326
+ mqttAppend(body, mqttUtf8Field(topicFilter))
327
+ return mqttFrame(MQTT_PKT_UNSUBSCRIBE, 0x02, body.toArray())
328
+ }
329
+
330
+ /**
331
+ * 组「只带报文标识符」的确认:PUBACK / PUBREC / PUBCOMP 标志为 0,
332
+ * PUBREL 的标志协议固定为 0b0010(QoS 2 的来回每一步都得按这条来,标志错了对端会当非法报文丢弃)。
333
+ */
334
+ public func mqttEncodeAck(typeCode: UInt8, packetId: Int64): Array<UInt8> {
335
+ let body = ArrayList<UInt8>()
336
+ mqttPutU16(body, mqttClampInt(packetId, 1, 65535))
337
+ var flags: Int64 = 0
338
+ if (typeCode == MQTT_PKT_PUBREL) {
339
+ flags = 0x02
340
+ }
341
+ return mqttFrame(typeCode, flags, body.toArray())
342
+ }
343
+
344
+ /** 组 PINGREQ:保活心跳,剩余长度为 0 */
345
+ public func mqttEncodePingreq(): Array<UInt8> {
346
+ return mqttFrame(MQTT_PKT_PINGREQ, 0, Array<UInt8>())
347
+ }
348
+
349
+ /** 组 DISCONNECT:正常下线(broker 收到后不会代发遗嘱) */
350
+ public func mqttEncodeDisconnect(): Array<UInt8> {
351
+ return mqttFrame(MQTT_PKT_DISCONNECT, 0, Array<UInt8>())
352
+ }
353
+
354
+ // ---------------------------------------------------------------- 解码
355
+
356
+ /** 解出来的一条控制报文:类型/标志 + 变长头与载荷(具体语义交给下面的 parseXxx) */
357
+ public class MqttPacket {
358
+ public var typeCode: UInt8 = 0
359
+ public var flags: Int64 = 0
360
+ public var body: Array<UInt8> = Array<UInt8>()
361
+ }
362
+
363
+ /**
364
+ * 解一条完整报文(固定头 + 剩余长度 + body 都已凑齐;凑字节是读循环的活)
365
+ *
366
+ * 长度字段与实收字节数对不上、类型码不在 1..14 → None。**宁可拒一条畸形报文也不猜它的意图**:
367
+ * 猜出来的「半条报文」进了状态机,现象只会是更难查的错位。
368
+ */
369
+ public func mqttDecodePacket(raw: Array<UInt8>): Option<MqttPacket> {
370
+ if (raw.size < 2) {
371
+ return None
372
+ }
373
+ let typeCode = UInt8(Int64(raw[0]) >> 4)
374
+ let flags = Int64(raw[0]) & 0x0F
375
+ if (typeCode < MQTT_PKT_CONNECT || typeCode > MQTT_PKT_DISCONNECT) {
376
+ return None
377
+ }
378
+ match (mqttDecodeRemainingLength(raw, 1)) {
379
+ case None => return None
380
+ case Some(rl) =>
381
+ let start = 1 + rl.consumed
382
+ if (rl.value != Int64(raw.size) - start) {
383
+ return None
384
+ }
385
+ let p = MqttPacket()
386
+ p.typeCode = typeCode
387
+ p.flags = flags
388
+ p.body = raw[start..raw.size]
389
+ return Some(p)
390
+ }
391
+ }
392
+
393
+ /** CONNACK 的内容:会话是否还挂在 broker 上 + 返回码(0 = 接受) */
394
+ public class MqttConnack {
395
+ public var sessionPresent: Bool = false
396
+ public var returnCode: Int64 = -1
397
+ }
398
+
399
+ /** 解 CONNACK(协议里它是 2 字节定长:会话标志 + 返回码) */
400
+ public func mqttParseConnack(p: MqttPacket): Option<MqttConnack> {
401
+ if (p.typeCode != MQTT_PKT_CONNACK || p.body.size < 2) {
402
+ return None
403
+ }
404
+ let c = MqttConnack()
405
+ c.sessionPresent = (Int64(p.body[0]) & 0x01) != 0
406
+ c.returnCode = Int64(p.body[1])
407
+ return Some(c)
408
+ }
409
+
410
+ /** 解 SUBACK:授予的 QoS 列表(0x80 = 该过滤器订阅失败);首 2 字节是报文标识符 */
411
+ public func mqttParseSuback(p: MqttPacket): Option<ArrayList<Int64>> {
412
+ if (p.typeCode != MQTT_PKT_SUBACK || p.body.size < 3) {
413
+ return None
414
+ }
415
+ let granted = ArrayList<Int64>()
416
+ var i: Int64 = 2
417
+ while (i < p.body.size) {
418
+ granted.add(Int64(p.body[i]))
419
+ i += 1
420
+ }
421
+ return Some(granted)
422
+ }
423
+
424
+ /** 解出来的 PUBLISH */
425
+ public class MqttPublish {
426
+ public var topic: String = ""
427
+ public var payload: Array<UInt8> = Array<UInt8>()
428
+ public var qos: Int64 = 0
429
+ public var retain: Bool = false
430
+ public var dup: Bool = false
431
+ public var packetId: Int64 = 0
432
+ public var hasPacketId: Bool = false
433
+ }
434
+
435
+ /**
436
+ * 解 PUBLISH:主题是 UTF-8 字符串(2 字节长度 + 字节内容),QoS>0 时随后跟 2 字节报文标识符,
437
+ * 余下的全是载荷(二进制,可能是图/结构体,所以这里不试解码文本)。
438
+ * 标志位里的 QoS 若是 3(协议保留值)→ None:这条报文按协议就是非法的。
439
+ */
440
+ public func mqttParsePublish(p: MqttPacket): Option<MqttPublish> {
441
+ if (p.typeCode != MQTT_PKT_PUBLISH || p.body.size < 2) {
442
+ return None
443
+ }
444
+ let qosRaw = (p.flags >> 1) & 0x03
445
+ if (qosRaw > 2) {
446
+ return None
447
+ }
448
+ let tlen = mqttGetU16(p.body, 0)
449
+ if (tlen < 0 || 2 + tlen > p.body.size) {
450
+ return None
451
+ }
452
+ let out = MqttPublish()
453
+ out.topic = String.fromUtf8(p.body[2..2 + tlen])
454
+ out.qos = qosRaw
455
+ out.retain = (p.flags & 0x01) != 0
456
+ out.dup = (p.flags & 0x08) != 0
457
+ var at = 2 + tlen
458
+ if (out.qos > 0) {
459
+ if (at + 2 > p.body.size) {
460
+ return None
461
+ }
462
+ out.packetId = mqttGetU16(p.body, at)
463
+ out.hasPacketId = true
464
+ at += 2
465
+ }
466
+ out.payload = p.body[at..p.body.size]
467
+ return Some(out)
468
+ }
469
+
470
+ /** 取「只带报文标识符」的确认报文里的标识符(PUBACK/PUBREC/PUBREL/PUBCOMP/UNSUBACK 同形);不足 2 字节回 -1 */
471
+ public func mqttParsePacketId(p: MqttPacket): Int64 {
472
+ if (p.body.size < 2) {
473
+ return -1
474
+ }
475
+ return mqttGetU16(p.body, 0)
476
+ }
477
+
478
+ // ---------------------------------------------------------------- 参数校验与展示助手
479
+
480
+ /** 主机名校验:只收 IP / 主机名的安全字符(页面可控字符串要进 socket,先在这儿挡一道) */
481
+ public func mqttValidHost(h: String): Bool {
482
+ if (h.size == 0 || h.size > 253) {
483
+ return false
484
+ }
485
+ for (b in h.toArray()) {
486
+ let c = Int64(b)
487
+ let digit = c >= 0x30 && c <= 0x39
488
+ let upper = c >= 0x41 && c <= 0x5A
489
+ let lower = c >= 0x61 && c <= 0x7A
490
+ let punct = c == 0x2E || c == 0x2D || c == 0x5F // 点 / 连字符 / 下划线
491
+ if (!digit && !upper && !lower && !punct) {
492
+ return false
493
+ }
494
+ }
495
+ return true
496
+ }
497
+
498
+ /** 端口校验:1..65535(页面可能传 0 / 负数 / 超大值) */
499
+ public func mqttValidPort(p: Int64): Bool {
500
+ return p >= 1 && p <= 65535
501
+ }
502
+
503
+ /** 发布主题:非空、不超过 65535 字节、不含通配符(`+` 与 `#` 在 PUBLISH 里是非法字符) */
504
+ public func mqttValidPublishTopic(t: String): Bool {
505
+ if (t.size == 0 || Int64(t.size) > MQTT_MAX_UTF8_FIELD) {
506
+ return false
507
+ }
508
+ if (t.contains("+") || t.contains("#")) {
509
+ return false
510
+ }
511
+ return true
512
+ }
513
+
514
+ /**
515
+ * 订阅过滤器:通配符必须**独占一整层**——`sensor/+/temp` 合法,`sensor+/temp` 非法;
516
+ * `#` 只能独占最后一层(`sensor/#` 合法,`sensor/#/x` 与 `sensor/#suffix` 都非法)。
517
+ *
518
+ * 逐层扫字节而不是 `split('/')`:层级内容可能含多字节 UTF-8,按字节扫能一眼看清「本层有几个字节、
519
+ * 里面有没有通配符」,也避开「分层后还要回头判断是不是最后一层」。**不校验的代价很实在**:非法过滤器
520
+ * 会被 broker 直接断连,现场表现是「一订阅就掉线」,很难往参数上想。
521
+ */
522
+ public func mqttValidTopicFilter(t: String): Bool {
523
+ let raw = t.toArray()
524
+ if (raw.size == 0 || Int64(raw.size) > MQTT_MAX_UTF8_FIELD) {
525
+ return false
526
+ }
527
+ let slash = 0x2F
528
+ let hash = 0x23
529
+ let plus = 0x2B
530
+ var start: Int64 = 0
531
+ while (start <= Int64(raw.size)) {
532
+ var end = start
533
+ var hasHash = false
534
+ var hasPlus = false
535
+ while (end < Int64(raw.size) && Int64(raw[end]) != slash) {
536
+ let c = Int64(raw[end])
537
+ if (c == hash) {
538
+ hasHash = true
539
+ }
540
+ if (c == plus) {
541
+ hasPlus = true
542
+ }
543
+ end += 1
544
+ }
545
+ let levelLen = end - start
546
+ let isLast = end == Int64(raw.size)
547
+ if (hasHash) {
548
+ if (levelLen != 1 || !isLast) {
549
+ return false
550
+ }
551
+ } else if (hasPlus) {
552
+ if (levelLen != 1) {
553
+ return false
554
+ }
555
+ }
556
+ if (isLast) {
557
+ return true
558
+ }
559
+ start = end + 1
560
+ }
561
+ return true
562
+ }
563
+
564
+ /** CONNACK 返回码 → 人话(页面直接把这段文案显示出来,不用自己攒一张表) */
565
+ public func mqttReturnCodeText(code: Int64): String {
566
+ if (code == 0) {
567
+ return "连接已接受"
568
+ }
569
+ if (code == 1) {
570
+ return "协议级别不被支持(对端不是 MQTT 3.1.1 服务端?)"
571
+ }
572
+ if (code == 2) {
573
+ return "客户端标识符被拒绝"
574
+ }
575
+ if (code == 3) {
576
+ return "服务端不可用"
577
+ }
578
+ if (code == 4) {
579
+ return "用户名或密码错误"
580
+ }
581
+ if (code == 5) {
582
+ return "未授权"
583
+ }
584
+ return "未知返回码(${code})"
585
+ }
586
+
587
+ /**
588
+ * 控制报文类型名(日志行与页面「方向/类型」列用的都是它)
589
+ *
590
+ * 用 if 链而不是 match:类型码是 UInt8,拿常量当 match 模式对不上时编译器不会提醒,
591
+ * if 链至少能一眼看出每个分支的取值范围。
592
+ */
593
+ public func mqttPacketTypeName(typeCode: UInt8): String {
594
+ if (typeCode == MQTT_PKT_CONNECT) {
595
+ return "CONNECT"
596
+ }
597
+ if (typeCode == MQTT_PKT_CONNACK) {
598
+ return "CONNACK"
599
+ }
600
+ if (typeCode == MQTT_PKT_PUBLISH) {
601
+ return "PUBLISH"
602
+ }
603
+ if (typeCode == MQTT_PKT_PUBACK) {
604
+ return "PUBACK"
605
+ }
606
+ if (typeCode == MQTT_PKT_PUBREC) {
607
+ return "PUBREC"
608
+ }
609
+ if (typeCode == MQTT_PKT_PUBREL) {
610
+ return "PUBREL"
611
+ }
612
+ if (typeCode == MQTT_PKT_PUBCOMP) {
613
+ return "PUBCOMP"
614
+ }
615
+ if (typeCode == MQTT_PKT_SUBSCRIBE) {
616
+ return "SUBSCRIBE"
617
+ }
618
+ if (typeCode == MQTT_PKT_SUBACK) {
619
+ return "SUBACK"
620
+ }
621
+ if (typeCode == MQTT_PKT_UNSUBSCRIBE) {
622
+ return "UNSUBSCRIBE"
623
+ }
624
+ if (typeCode == MQTT_PKT_UNSUBACK) {
625
+ return "UNSUBACK"
626
+ }
627
+ if (typeCode == MQTT_PKT_PINGREQ) {
628
+ return "PINGREQ"
629
+ }
630
+ if (typeCode == MQTT_PKT_PINGRESP) {
631
+ return "PINGRESP"
632
+ }
633
+ if (typeCode == MQTT_PKT_DISCONNECT) {
634
+ return "DISCONNECT"
635
+ }
636
+ return "UNKNOWN(${typeCode})"
637
+ }
638
+
639
+ /**
640
+ * 载荷是不是「能直接看的文本」:不是合法 UTF-8、或含 NUL/控制字符(制表/换行/回车除外)→ None,
641
+ * 页面据此改用十六进制显示。**不做「猜编码」**——MQTT 的载荷可能是任意二进制,猜错比不猜更误导人。
642
+ */
643
+ public func mqttBytesToText(bytes: Array<UInt8>): Option<String> {
644
+ if (bytes.size == 0) {
645
+ return Some("")
646
+ }
647
+ var text = ""
648
+ try {
649
+ text = String.fromUtf8(bytes)
650
+ } catch (e: Exception) {
651
+ return None
652
+ }
653
+ for (b in bytes) {
654
+ let c = Int64(b)
655
+ let control = c < 0x20 && c != 0x09 && c != 0x0A && c != 0x0D
656
+ if (c == 0 || control || c == 0x7F) {
657
+ return None
658
+ }
659
+ }
660
+ return Some(text)
661
+ }