cj-tauri 0.8.0 → 1.1.1

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 +53 -7
  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
@@ -1,267 +1,269 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import std.collection.*
5
- import stdx.encoding.json.*
6
- import std.unittest.*
7
- import std.unittest.testmacro.*
8
-
9
- /*
10
- * serial 插件单元测试:纯函数(档位 / 校验位文案 / 路径白名单 / hex / 文本)+ 命令面参数校验
11
- * + 命名权限集的闸门。
12
- *
13
- * 为什么这里不测真的读写:设备 I/O 在 C 桥里(native/bridge_linux.c 的 termios 层),
14
- * 需要真实设备节点或伪终端 —— 归实机验证(examples/plugin-serial + socat 造的虚拟串口对)。
15
- * 这里只锁「不碰设备就能判定的部分」,且这几条正是页面最容易踩错的入口。
16
- */
17
-
18
- /** 造带路径的 open 参数 */
19
- func serialOpenArgs(path: String): JsonObject {
20
- var o = JsonObject()
21
- o.put("path", JsonString(path))
22
- return o
23
- }
24
-
25
- /** 取一条命令处理器 */
26
- func serialCmd(short: String): CommandHandler {
27
- return SerialPlugin().commands().get(short).getOrThrow()
28
- }
29
-
30
- /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
31
- func serialErrOf(short: String, args: JsonObject): String {
32
- try {
33
- serialCmd(short).handle("serial:${short}", args, IpcContext("main", IpcHub()))
34
- return ""
35
- } catch (e: CommandException) {
36
- return e.message
37
- }
38
- }
39
-
40
- @Test
41
- func testSerialPluginSurface(): Unit {
42
- let p = SerialPlugin()
43
- @Expect(p.name(), "serial")
44
- let names = pluginCommandNames(p)
45
- @Expect(names.size, 5)
46
- var found = 0
47
- for (n in names) {
48
- if (n == "serial:list" || n == "serial:open" || n == "serial:read" || n == "serial:write" || n == "serial:close") {
49
- found++
50
- }
51
- }
52
- @Expect(found, 5)
53
- }
54
-
55
- @Test
56
- func testSerialPluginShimTargetsItsOwnNamespace(): Unit {
57
- let js = SerialPlugin().jsShim()
58
- @Expect(js.contains("t.serial"), true)
59
- @Expect(js.contains("'serial:open'"), true)
60
- @Expect(js.contains("'serial:write'"), true)
61
- // shim 只挂自己的命名空间:不该动 invoke / listen / emit
62
- @Expect(js.contains("invoke('serial:"), true)
63
- }
64
-
65
- @Test
66
- func testSerialPermissionSetsGateWrite(): Unit {
67
- let sets = pluginPermissionSets(SerialPlugin())
68
- @Expect(sets.size, 2)
69
- let ro = sets.get("serial:readonly").getOrThrow()
70
- @Expect(ro.size, 4)
71
- var roHasWrite = false
72
- for (m in ro) {
73
- if (m == "serial:write") {
74
- roHasWrite = true
75
- }
76
- }
77
- // 只想读设备的应用不该顺带拿到写能力
78
- @Expect(roHasWrite, false)
79
- @Expect(sets.get("serial:default").getOrThrow().size, 5)
80
- }
81
-
82
- @Test
83
- func testSerialBaudTableMatchesBridge(): Unit {
84
- @Expect(serialBaudSupported(9600), true)
85
- @Expect(serialBaudSupported(115200), true)
86
- @Expect(serialBaudSupported(1000000), true)
87
- // 档位之外一律不收(C 侧 termios 也没有对应宏,收了只会变成运行期报错)
88
- @Expect(serialBaudSupported(12345), false)
89
- @Expect(serialBaudSupported(0), false)
90
- @Expect(serialBaudSupported(-1), false)
91
- let text = serialBaudListText()
92
- @Expect(text.contains("115200"), true)
93
- @Expect(text.startsWith("300/"), true)
94
- @Expect(text.endsWith("1000000"), true)
95
- }
96
-
97
- @Test
98
- func testSerialParityTextMapping(): Unit {
99
- @Expect(serialParityCode("none"), 0)
100
- @Expect(serialParityCode("n"), 0)
101
- @Expect(serialParityCode("odd"), 1)
102
- @Expect(serialParityCode("o"), 1)
103
- @Expect(serialParityCode("even"), 2)
104
- @Expect(serialParityCode("e"), 2)
105
- // 大小写不敏感:前端从配置里读到的常是大写
106
- @Expect(serialParityCode("None"), 0)
107
- @Expect(serialParityCode("EVEN"), 2)
108
- @Expect(serialParityCode("mark"), -1)
109
- @Expect(serialParityCode(""), -1)
110
- }
111
-
112
- @Test
113
- func testSerialPathWhitelistMirror(): Unit {
114
- @Expect(serialPathLooksAllowed("/dev/ttyUSB0"), true)
115
- @Expect(serialPathLooksAllowed("/dev/ttyACM0"), true)
116
- @Expect(serialPathLooksAllowed("/dev/ttyS3"), true)
117
- @Expect(serialPathLooksAllowed("/dev/pts/7"), true)
118
- @Expect(serialPathLooksAllowed("/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0"), true)
119
- @Expect(serialPathLooksAllowed("/dev/cu.usbserial-1420"), true)
120
- @Expect(serialPathLooksAllowed("COM3"), true)
121
- @Expect(serialPathLooksAllowed("\\\\.\\COM12"), true)
122
-
123
- @Expect(serialPathLooksAllowed("/etc/shadow"), false)
124
- @Expect(serialPathLooksAllowed("/dev/tty"), false)
125
- @Expect(serialPathLooksAllowed("/dev/ttyS"), false)
126
- @Expect(serialPathLooksAllowed("/dev/ttyUSB0/../../etc/shadow"), false)
127
- @Expect(serialPathLooksAllowed("/dev/serial/by-id/../../etc/passwd"), false)
128
- @Expect(serialPathLooksAllowed("COMfoo"), false)
129
- @Expect(serialPathLooksAllowed("COM1234"), false)
130
- @Expect(serialPathLooksAllowed(""), false)
131
- }
132
-
133
- @Test
134
- func testSerialHexAndTextConversions(): Unit {
135
- let bytes = [UInt8(0), UInt8(15), UInt8(165), UInt8(255)]
136
- @Expect(serialBytesToHex(bytes), "000fa5ff")
137
-
138
- let back = serialHexToBytes("000fa5ff").getOrThrow()
139
- @Expect(back.size, 4)
140
- @Expect(serialBytesToHex(back), "000fa5ff")
141
- // 大写十六进制也认
142
- @Expect(serialBytesToHex(serialHexToBytes("A5").getOrThrow()), "a5")
143
- // 奇数长度 / 非十六进制字符一律拒绝,不猜
144
- @Expect(serialHexToBytes("0ff").isNone(), true)
145
- @Expect(serialHexToBytes("zz").isNone(), true)
146
- @Expect(serialHexToBytes("").getOrThrow().size, 0)
147
-
148
- @Expect(serialBytesToText("hi".toArray()).getOrThrow(), "hi")
149
- // 二进制帧(不是合法 UTF-8)不该硬塞成一个坏字符串
150
- @Expect(serialBytesToText([UInt8(255), UInt8(254)]).isNone(), true)
151
- }
152
-
153
- @Test
154
- func testSerialOpenRejectsBadArgumentsBeforeTouchingDevice(): Unit {
155
- // 白名单在仓颉侧先给一句人话;真正的边界在 C 桥,但错误文案要能定位
156
- @Expect(serialErrOf("open", serialOpenArgs("/etc/shadow")).contains("不在允许的设备名单内"), true)
157
- // 缺参数
158
- @Expect(serialErrOf("open", JsonObject()).contains("missing argument \"path\""), true)
159
-
160
- var args = serialOpenArgs("/dev/ttyUSB0")
161
- args.put("baud", JsonInt(12345))
162
- @Expect(serialErrOf("open", args).contains("不支持的波特率"), true)
163
-
164
- args = serialOpenArgs("/dev/ttyUSB0")
165
- args.put("dataBits", JsonInt(9))
166
- @Expect(serialErrOf("open", args).contains("dataBits"), true)
167
-
168
- args = serialOpenArgs("/dev/ttyUSB0")
169
- args.put("parity", JsonString("mark"))
170
- @Expect(serialErrOf("open", args).contains("parity"), true)
171
-
172
- args = serialOpenArgs("/dev/ttyUSB0")
173
- args.put("stopBits", JsonInt(3))
174
- @Expect(serialErrOf("open", args).contains("stopBits"), true)
175
- }
176
-
177
- @Test
178
- func testSerialHandleAndPayloadValidation(): Unit {
179
- // 句柄缺失 / 不是 open 给的
180
- @Expect(serialErrOf("read", JsonObject()).contains("missing argument \"handle\""), true)
181
- var args = JsonObject()
182
- args.put("handle", JsonInt(0))
183
- @Expect(serialErrOf("read", args).contains("must be a handle returned by serial:open"), true)
184
- @Expect(serialErrOf("close", args).contains("must be a handle returned by serial:open"), true)
185
-
186
- args = JsonObject()
187
- args.put("handle", JsonInt(1))
188
- @Expect(serialErrOf("write", args).contains("missing argument \"data\""), true)
189
-
190
- args.put("data", JsonString("hi"))
191
- args.put("encoding", JsonString("base64"))
192
- @Expect(serialErrOf("write", args).contains("encoding 只认"), true)
193
-
194
- args = JsonObject()
195
- args.put("handle", JsonInt(1))
196
- args.put("data", JsonString("abc"))
197
- args.put("encoding", JsonString("hex"))
198
- @Expect(serialErrOf("write", args).contains("不是合法十六进制串"), true)
199
-
200
- // 超长写入在仓颉侧就挡住(单次上限与 read 的 maxBytes 同量级)
201
- var big = ""
202
- var i = 0
203
- while (i < 4097) {
204
- big = big + "a"
205
- i++
206
- }
207
- args = JsonObject()
208
- args.put("handle", JsonInt(1))
209
- args.put("data", JsonString(big))
210
- @Expect(serialErrOf("write", args).contains("单次最多 4096 字节"), true)
211
- }
212
-
213
- @Test
214
- func testSerialRejectsUnknownCommand(): Unit {
215
- var threw = false
216
- try {
217
- serialCmd("open").handle("serial:nope", JsonObject(), IpcContext("main", IpcHub()))
218
- } catch (e: CommandException) {
219
- threw = true
220
- @Expect(e.message.contains("unknown serial command"), true)
221
- }
222
- @Expect(threw, true)
223
- }
224
-
225
- @Test
226
- func testSerialPermissionSetGatesCommands(): Unit {
227
- let out = ArrayList<String>()
228
- let reg = CapabilityRegistry()
229
- reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"s\",\"permissions\":[\"serial:readonly\"]}"))
230
- let hub = hubWithRegistry(reg, out)
231
- registerPluginCommands(hub, SerialPlugin())
232
- registerPluginPermissionSets(reg, SerialPlugin())
233
-
234
- // 集内命令放行:没有参数 → 报的是业务错误(缺 handle),不是「未授权」
235
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"serial:read\"}")
236
- waitSink(out, 1)
237
- @Expect(out.size, 1)
238
- @Expect(strField(out[0], "error").contains("command not allowed"), false)
239
-
240
- // 写命令不在 readonly 集内:仍然拒
241
- hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"serial:write\"}")
242
- @Expect(out.size, 2)
243
- @Expect(strField(out[1], "error"), "command not allowed: serial:write")
244
- }
245
-
246
- @Test
247
- func testSerialPortListIsSortedAndWhitelisted(): Unit {
248
- let ports = serialPortList()
249
- // 再排一次应逐元素相等:证明输出有序且无重复(页面下拉按这个顺序渲染)
250
- let sorted = ArrayList<String>()
251
- var i = 0
252
- while (i < ports.size) {
253
- sorted.add(ports[i])
254
- i++
255
- }
256
- sorted.sort()
257
- @Expect(sorted.size, ports.size)
258
- i = 0
259
- while (i < ports.size) {
260
- @Expect(sorted[i], ports[i])
261
- // 枚举出的端口必须过得了 C 桥白名单——列表与白名单是同一套设备面
262
- @Expect(serialPathLooksAllowed(ports[i]), true)
263
- // 伪终端(探针用的 pty)不算「本机串口」,不进下拉(白名单仍放行它)
264
- @Expect(ports[i].contains("/pts/"), false)
265
- i++
266
- }
267
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.plugins.serial.*
5
+ import std.collection.*
6
+ import std.sort.*
7
+ import stdx.encoding.json.*
8
+ import std.unittest.*
9
+ import std.unittest.testmacro.*
10
+
11
+ /*
12
+ * serial 插件单元测试:纯函数(档位 / 校验位文案 / 路径白名单 / hex / 文本)+ 命令面参数校验
13
+ * + 命名权限集的闸门。
14
+ *
15
+ * 为什么这里不测真的读写:设备 I/O 在 C 桥里(native/bridge_linux.c 的 termios 层),
16
+ * 需要真实设备节点或伪终端 —— 归实机验证(examples/plugin-serial + socat 造的虚拟串口对)。
17
+ * 这里只锁「不碰设备就能判定的部分」,且这几条正是页面最容易踩错的入口。
18
+ */
19
+
20
+ /** 造带路径的 open 参数 */
21
+ func serialOpenArgs(path: String): JsonObject {
22
+ var o = JsonObject()
23
+ o.put("path", JsonString(path))
24
+ return o
25
+ }
26
+
27
+ /** 取一条命令处理器 */
28
+ func serialCmd(short: String): CommandHandler {
29
+ return SerialPlugin().commands().get(short).getOrThrow()
30
+ }
31
+
32
+ /** 直接调命令,把 CommandException 的原文取出来(没抛就返回空串)*/
33
+ func serialErrOf(short: String, args: JsonObject): String {
34
+ try {
35
+ serialCmd(short).handle("serial:${short}", args, IpcContext("main", IpcHub()))
36
+ return ""
37
+ } catch (e: CommandException) {
38
+ return e.message
39
+ }
40
+ }
41
+
42
+ @Test
43
+ func testSerialPluginSurface(): Unit {
44
+ let p = SerialPlugin()
45
+ @Expect(p.name(), "serial")
46
+ let names = pluginCommandNames(p)
47
+ @Expect(names.size, 5)
48
+ var found = 0
49
+ for (n in names) {
50
+ if (n == "serial:list" || n == "serial:open" || n == "serial:read" || n == "serial:write" || n == "serial:close") {
51
+ found++
52
+ }
53
+ }
54
+ @Expect(found, 5)
55
+ }
56
+
57
+ @Test
58
+ func testSerialPluginShimTargetsItsOwnNamespace(): Unit {
59
+ let js = SerialPlugin().jsShim()
60
+ @Expect(js.contains("t.serial"), true)
61
+ @Expect(js.contains("'serial:open'"), true)
62
+ @Expect(js.contains("'serial:write'"), true)
63
+ // shim 只挂自己的命名空间:不该动 invoke / listen / emit
64
+ @Expect(js.contains("invoke('serial:"), true)
65
+ }
66
+
67
+ @Test
68
+ func testSerialPermissionSetsGateWrite(): Unit {
69
+ let sets = pluginPermissionSets(SerialPlugin())
70
+ @Expect(sets.size, 2)
71
+ let ro = sets.get("serial:readonly").getOrThrow()
72
+ @Expect(ro.size, 4)
73
+ var roHasWrite = false
74
+ for (m in ro) {
75
+ if (m == "serial:write") {
76
+ roHasWrite = true
77
+ }
78
+ }
79
+ // 只想读设备的应用不该顺带拿到写能力
80
+ @Expect(roHasWrite, false)
81
+ @Expect(sets.get("serial:default").getOrThrow().size, 5)
82
+ }
83
+
84
+ @Test
85
+ func testSerialBaudTableMatchesBridge(): Unit {
86
+ @Expect(serialBaudSupported(9600), true)
87
+ @Expect(serialBaudSupported(115200), true)
88
+ @Expect(serialBaudSupported(1000000), true)
89
+ // 档位之外一律不收(C 侧 termios 也没有对应宏,收了只会变成运行期报错)
90
+ @Expect(serialBaudSupported(12345), false)
91
+ @Expect(serialBaudSupported(0), false)
92
+ @Expect(serialBaudSupported(-1), false)
93
+ let text = serialBaudListText()
94
+ @Expect(text.contains("115200"), true)
95
+ @Expect(text.startsWith("300/"), true)
96
+ @Expect(text.endsWith("1000000"), true)
97
+ }
98
+
99
+ @Test
100
+ func testSerialParityTextMapping(): Unit {
101
+ @Expect(serialParityCode("none"), 0)
102
+ @Expect(serialParityCode("n"), 0)
103
+ @Expect(serialParityCode("odd"), 1)
104
+ @Expect(serialParityCode("o"), 1)
105
+ @Expect(serialParityCode("even"), 2)
106
+ @Expect(serialParityCode("e"), 2)
107
+ // 大小写不敏感:前端从配置里读到的常是大写
108
+ @Expect(serialParityCode("None"), 0)
109
+ @Expect(serialParityCode("EVEN"), 2)
110
+ @Expect(serialParityCode("mark"), -1)
111
+ @Expect(serialParityCode(""), -1)
112
+ }
113
+
114
+ @Test
115
+ func testSerialPathWhitelistMirror(): Unit {
116
+ @Expect(serialPathLooksAllowed("/dev/ttyUSB0"), true)
117
+ @Expect(serialPathLooksAllowed("/dev/ttyACM0"), true)
118
+ @Expect(serialPathLooksAllowed("/dev/ttyS3"), true)
119
+ @Expect(serialPathLooksAllowed("/dev/pts/7"), true)
120
+ @Expect(serialPathLooksAllowed("/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0"), true)
121
+ @Expect(serialPathLooksAllowed("/dev/cu.usbserial-1420"), true)
122
+ @Expect(serialPathLooksAllowed("COM3"), true)
123
+ @Expect(serialPathLooksAllowed("\\\\.\\COM12"), true)
124
+
125
+ @Expect(serialPathLooksAllowed("/etc/shadow"), false)
126
+ @Expect(serialPathLooksAllowed("/dev/tty"), false)
127
+ @Expect(serialPathLooksAllowed("/dev/ttyS"), false)
128
+ @Expect(serialPathLooksAllowed("/dev/ttyUSB0/../../etc/shadow"), false)
129
+ @Expect(serialPathLooksAllowed("/dev/serial/by-id/../../etc/passwd"), false)
130
+ @Expect(serialPathLooksAllowed("COMfoo"), false)
131
+ @Expect(serialPathLooksAllowed("COM1234"), false)
132
+ @Expect(serialPathLooksAllowed(""), false)
133
+ }
134
+
135
+ @Test
136
+ func testSerialHexAndTextConversions(): Unit {
137
+ let bytes = [UInt8(0), UInt8(15), UInt8(165), UInt8(255)]
138
+ @Expect(bytesToHex(bytes), "000fa5ff")
139
+
140
+ let back = hexToBytes("000fa5ff").getOrThrow()
141
+ @Expect(back.size, 4)
142
+ @Expect(bytesToHex(back), "000fa5ff")
143
+ // 大写十六进制也认
144
+ @Expect(bytesToHex(hexToBytes("A5").getOrThrow()), "a5")
145
+ // 奇数长度 / 非十六进制字符一律拒绝,不猜
146
+ @Expect(hexToBytes("0ff").isNone(), true)
147
+ @Expect(hexToBytes("zz").isNone(), true)
148
+ @Expect(hexToBytes("").getOrThrow().size, 0)
149
+
150
+ @Expect(serialBytesToText("hi".toArray()).getOrThrow(), "hi")
151
+ // 二进制帧(不是合法 UTF-8)不该硬塞成一个坏字符串
152
+ @Expect(serialBytesToText([UInt8(255), UInt8(254)]).isNone(), true)
153
+ }
154
+
155
+ @Test
156
+ func testSerialOpenRejectsBadArgumentsBeforeTouchingDevice(): Unit {
157
+ // 白名单在仓颉侧先给一句人话;真正的边界在 C 桥,但错误文案要能定位
158
+ @Expect(serialErrOf("open", serialOpenArgs("/etc/shadow")).contains("不在允许的设备名单内"), true)
159
+ // 缺参数
160
+ @Expect(serialErrOf("open", JsonObject()).contains("missing argument \"path\""), true)
161
+
162
+ var args = serialOpenArgs("/dev/ttyUSB0")
163
+ args.put("baud", JsonInt(12345))
164
+ @Expect(serialErrOf("open", args).contains("不支持的波特率"), true)
165
+
166
+ args = serialOpenArgs("/dev/ttyUSB0")
167
+ args.put("dataBits", JsonInt(9))
168
+ @Expect(serialErrOf("open", args).contains("dataBits"), true)
169
+
170
+ args = serialOpenArgs("/dev/ttyUSB0")
171
+ args.put("parity", JsonString("mark"))
172
+ @Expect(serialErrOf("open", args).contains("parity"), true)
173
+
174
+ args = serialOpenArgs("/dev/ttyUSB0")
175
+ args.put("stopBits", JsonInt(3))
176
+ @Expect(serialErrOf("open", args).contains("stopBits"), true)
177
+ }
178
+
179
+ @Test
180
+ func testSerialHandleAndPayloadValidation(): Unit {
181
+ // 句柄缺失 / 不是 open 给的
182
+ @Expect(serialErrOf("read", JsonObject()).contains("missing argument \"handle\""), true)
183
+ var args = JsonObject()
184
+ args.put("handle", JsonInt(0))
185
+ @Expect(serialErrOf("read", args).contains("must be a handle returned by serial:open"), true)
186
+ @Expect(serialErrOf("close", args).contains("must be a handle returned by serial:open"), true)
187
+
188
+ args = JsonObject()
189
+ args.put("handle", JsonInt(1))
190
+ @Expect(serialErrOf("write", args).contains("missing argument \"data\""), true)
191
+
192
+ args.put("data", JsonString("hi"))
193
+ args.put("encoding", JsonString("base64"))
194
+ @Expect(serialErrOf("write", args).contains("encoding 只认"), true)
195
+
196
+ args = JsonObject()
197
+ args.put("handle", JsonInt(1))
198
+ args.put("data", JsonString("abc"))
199
+ args.put("encoding", JsonString("hex"))
200
+ @Expect(serialErrOf("write", args).contains("不是合法十六进制串"), true)
201
+
202
+ // 超长写入在仓颉侧就挡住(单次上限与 read 的 maxBytes 同量级)
203
+ var big = ""
204
+ var i = 0
205
+ while (i < 4097) {
206
+ big = big + "a"
207
+ i++
208
+ }
209
+ args = JsonObject()
210
+ args.put("handle", JsonInt(1))
211
+ args.put("data", JsonString(big))
212
+ @Expect(serialErrOf("write", args).contains("单次最多 4096 字节"), true)
213
+ }
214
+
215
+ @Test
216
+ func testSerialRejectsUnknownCommand(): Unit {
217
+ var threw = false
218
+ try {
219
+ serialCmd("open").handle("serial:nope", JsonObject(), IpcContext("main", IpcHub()))
220
+ } catch (e: CommandException) {
221
+ threw = true
222
+ @Expect(e.message.contains("unknown serial command"), true)
223
+ }
224
+ @Expect(threw, true)
225
+ }
226
+
227
+ @Test
228
+ func testSerialPermissionSetGatesCommands(): Unit {
229
+ let out = ArrayList<String>()
230
+ let reg = CapabilityRegistry()
231
+ reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"s\",\"permissions\":[\"serial:readonly\"]}"))
232
+ let hub = hubWithRegistry(reg, out)
233
+ registerPluginCommands(hub, SerialPlugin())
234
+ registerPluginPermissionSets(reg, SerialPlugin())
235
+
236
+ // 集内命令放行:没有参数 → 报的是业务错误(缺 handle),不是「未授权」
237
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"serial:read\"}")
238
+ waitSink(out, 1)
239
+ @Expect(out.size, 1)
240
+ @Expect(strField(out[0], "error").contains("command not allowed"), false)
241
+
242
+ // 写命令不在 readonly 集内:仍然拒
243
+ hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"serial:write\"}")
244
+ @Expect(out.size, 2)
245
+ @Expect(strField(out[1], "error"), "command not allowed: serial:write")
246
+ }
247
+
248
+ @Test
249
+ func testSerialPortListIsSortedAndWhitelisted(): Unit {
250
+ let ports = serialPortList()
251
+ // 再排一次应逐元素相等:证明输出有序且无重复(页面下拉按这个顺序渲染)
252
+ let sorted = ArrayList<String>()
253
+ var i = 0
254
+ while (i < ports.size) {
255
+ sorted.add(ports[i])
256
+ i++
257
+ }
258
+ sort(sorted)
259
+ @Expect(sorted.size, ports.size)
260
+ i = 0
261
+ while (i < ports.size) {
262
+ @Expect(sorted[i], ports[i])
263
+ // 枚举出的端口必须过得了 C 桥白名单——列表与白名单是同一套设备面
264
+ @Expect(serialPathLooksAllowed(ports[i]), true)
265
+ // 伪终端(探针用的 pty)不算「本机串口」,不进下拉(白名单仍放行它)
266
+ @Expect(ports[i].contains("/pts/"), false)
267
+ i++
268
+ }
269
+ }