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.
- package/README.md +3 -3
- package/cjpm.toml +52 -25
- package/cli/cjpm.toml +1 -1
- package/cli/src/doctor.cj +304 -0
- package/cli/src/main.cj +33 -7
- package/cli/src/project.cj +59 -1
- package/cli/src/resolve.cj +348 -304
- package/cli/src/scaffold.cj +139 -22
- package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/README.md +79 -0
- package/cli/templates/ohos-pc/build-hap.sh +190 -0
- package/cli/templates/ohos-pc/build-profile.json5 +46 -0
- package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
- package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
- package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
- package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
- package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
- package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/oh-package.json5 +10 -0
- package/native/build_linux.sh +30 -2
- package/native/build_win.bat +62 -61
- package/native/core/bridge_core.c +1626 -0
- package/native/{bridge_core.h → core/bridge_core.h} +246 -6
- package/native/platforms/harmonyos/README.md +106 -0
- package/native/platforms/linux/bridge_linux.c +2485 -0
- package/native/platforms/windows/bridge_win.c +3248 -0
- package/native/tests/test_bridge_core.c +528 -1
- package/package.json +12 -3
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/api_system.cj +10 -2
- package/src/app.cj +248 -85
- package/src/arkweb_proxy.cj +56 -0
- package/src/arkweb_script.cj +154 -0
- package/src/capability.cj +140 -0
- package/src/clipboard_bridge.cj +55 -0
- package/src/dragdrop.cj +26 -0
- package/src/embedded_assets.cj +98 -0
- package/src/hexutil.cj +53 -0
- package/src/host.cj +197 -26
- package/src/host_arkweb.cj +740 -0
- package/src/host_factory.cj +48 -0
- package/src/host_webkit.cj +202 -26
- package/src/host_webview2.cj +176 -3
- package/src/hotkey.cj +66 -0
- package/src/ipc_hub.cj +49 -0
- package/src/ipc_message.cj +2 -1
- package/src/jsutil.cj +33 -0
- package/src/log_bridge.cj +107 -0
- package/src/logging.cj +127 -0
- package/src/menu.cj +197 -196
- package/src/plugin.cj +40 -0
- package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
- package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
- package/src/plugins/fs/plugin_fs.cj +194 -0
- package/src/plugins/http/plugin_http.cj +459 -0
- package/src/plugins/log/plugin_log.cj +105 -0
- package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
- package/src/plugins/mqtt/mqtt_codec.cj +661 -0
- package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
- package/src/plugins/notification/plugin_notification.cj +146 -0
- package/src/plugins/plugins.cj +5 -0
- package/src/plugins/redis/plugin_redis.cj +2409 -0
- package/src/plugins/redis/redis_codec.cj +556 -0
- package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
- package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
- package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
- package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
- package/src/plugins/store/plugin_store.cj +287 -0
- package/src/plugins/ws/plugin_ws.cj +470 -0
- package/src/process_hidden.cj +174 -0
- package/src/scope.cj +391 -0
- package/src/shell_event.cj +69 -68
- package/src/tests/capability_test.cj +53 -0
- package/src/tests/fake_host.cj +85 -20
- package/src/tests/ipc_hub_test.cj +13 -0
- package/src/tests/jsutil_test.cj +62 -0
- package/src/tests/logging_test.cj +62 -0
- package/src/tests/mqtt_codec_test.cj +405 -0
- package/src/tests/plugin_clipboard_test.cj +222 -0
- package/src/tests/plugin_dialog_test.cj +98 -4
- package/src/tests/plugin_http_test.cj +124 -0
- package/src/tests/plugin_menu_test.cj +1 -0
- package/src/tests/plugin_notification_test.cj +205 -0
- package/src/tests/plugin_serial_test.cj +269 -267
- package/src/tests/plugin_shell_test.cj +189 -188
- package/src/tests/plugin_sqlite_test.cj +230 -0
- package/src/tests/plugin_store_test.cj +260 -0
- package/src/tests/plugin_test.cj +542 -524
- package/src/tests/plugin_ws_test.cj +40 -0
- package/src/tests/process_hidden_test.cj +129 -0
- package/src/tests/redis_codec_test.cj +396 -0
- package/src/tests/scope_test.cj +350 -0
- package/src/tests/shell_event_test.cj +0 -1
- package/src/tests/system_host_test.cj +81 -79
- package/src/tests/urlutil_test.cj +53 -0
- package/src/tests/version_test.cj +75 -65
- package/src/tray.cj +90 -0
- package/src/urlutil.cj +159 -0
- package/src/version.cj +23 -2
- package/src/window.cj +51 -42
- package/native/bridge_core.c +0 -797
- package/native/bridge_linux.c +0 -1268
- package/native/bridge_win.c +0 -1633
- package/src/plugin_fs.cj +0 -118
- /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
|
+
}
|