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,556 @@
1
+ package cjTauri.plugins.redis
2
+
3
+ import cjTauri.*
4
+
5
+ import std.collection.*
6
+
7
+ /**
8
+ * Redis RESP2 协议的纯编解码层(只吃字节、只吐字节,不碰 socket)
9
+ *
10
+ * 为什么自己写:std / stdx 里都没有 Redis 协议,而 RESP2 的骨架很小——六种前缀(简单串 / 错误 /
11
+ * 整数 / 批量串 / 数组,加两个 null 变体)、长度一律十进制 + CRLF,几百行足够。设计取舍见
12
+ * `docs/设计决策记录.md` D-016。**编解码与连接状态机分开**:本文件全部无副作用、可单测;
13
+ * socket 读写、命令通道与订阅线程放在 `plugin_redis.cj`。
14
+ *
15
+ * 为什么不照 MQTT 那样「畸形就返回 None」:RESP 是**增量解析**的(一条回复可能跨好几个 socket 块),
16
+ * 「还没收全」与「协议非法」必须分开——前者接着等,后者当场拒。所以读取入口 `respTryRead()` 返回三态
17
+ * `RespRead`(`Incomplete` / `Value` / `Malformed`),并且**解析永远不抛异常**:对端发什么都不会
18
+ * 把宿主线程带走(畸形报文的抛异常路径只在编码侧,那是调用方自己的 bug)。
19
+ *
20
+ * 边界(如实):只做 RESP2。不做 RESP3 的 map(`%`)/ set(`~`)/ push(`>`)/ bool(`#`)/
21
+ * double(`,`)/ big number(`(`)/ verbatim string(`=`)/ attribute(`|`)——遇到这些前缀按
22
+ * `Malformed` 拒,**不猜它想表达什么**。也不做 inline 命令解析(我们只发数组形式)。
23
+ */
24
+
25
+ // ---------------------------------------------------------------- 常量
26
+
27
+ /** 前缀字符(RESP2 规范:`+` `-` `:` `$` `*`) */
28
+ public let RESP_SIMPLE: UInt8 = 43 // '+'
29
+ public let RESP_ERROR: UInt8 = 45 // '-'
30
+ public let RESP_INTEGER: UInt8 = 58 // ':'
31
+ public let RESP_BULK: UInt8 = 36 // '$'
32
+ public let RESP_ARRAY: UInt8 = 42 // '*'
33
+
34
+ /** 单个批量串上限 512MB(协议上限):只用来拦「对端谎报长度」把内存吃干 */
35
+ public let RESP_MAX_BULK: Int64 = 536870912
36
+
37
+ /** 嵌套层数上限:解析是递归的,深嵌套是廉价的内存炸弹 */
38
+ public let RESP_MAX_DEPTH: Int64 = 32
39
+
40
+ /** 单个数组的元素数上限(同样为了拦谎报) */
41
+ public let RESP_MAX_ELEMENTS: Int64 = 1048576
42
+
43
+ /** 消费游标超过这个数就把已读部分搬掉:长连接下缓冲区不该只涨不缩 */
44
+ let RESP_COMPACT_THRESHOLD: Int64 = 8192
45
+
46
+ // ---------------------------------------------------------------- 值模型
47
+
48
+ /**
49
+ * 一个 RESP2 值。null 用 `Option` 表达(`$-1` / `*-1`),不用哨兵——不然「空串」与「不存在」会撞在一起
50
+ * (Redis 里 `GET` 一个不存在的 key 返回 null bulk,`GET` 一个空串返回 0 字节 bulk,两者必须能分开)。
51
+ */
52
+ public enum RespValue {
53
+ | Simple(String) // +OK
54
+ | Failure(String) // -ERR unknown command
55
+ | Number(Int64) // :123
56
+ | Bulk(Option<Array<UInt8>>) // $5 hello / $-1 null
57
+ | Arr(Option<Array<RespValue>>) // *2 ... / *-1 null
58
+ }
59
+
60
+ /**
61
+ * 读取三态。**没有第四态**:「空缓冲」是 `Incomplete` 的特例,调用方按超时决定要不要放弃。
62
+ * `Malformed` 里带一句人话,直接可以进 stderr 取证行(对端发坏字节时得有凭证)。
63
+ */
64
+ public enum RespRead {
65
+ | Incomplete
66
+ | Value(RespValue)
67
+ | Malformed(String)
68
+ }
69
+
70
+ // ---------------------------------------------------------------- 解析核心
71
+
72
+ /**
73
+ * 增量读取器:`feed()` 喂 socket 收到的字节,`respTryRead()` 取下一个完整值。
74
+ *
75
+ * 解析成功才推进游标,否则原地不动(这是「半条报文跨块到达」能work的关键)。
76
+ * 内部是同一条字节缓冲,不做按值切分——调试工具的报文量级用不着更花的结构。
77
+ */
78
+ public class RespReader {
79
+ private var data: ArrayList<UInt8> = ArrayList<UInt8>()
80
+ private var cursor: Int64 = 0
81
+
82
+ public init() {}
83
+
84
+ /** 追加一块 socket 收到的字节(就地追加,不换缓冲) */
85
+ public func feed(chunk: Array<UInt8>): Unit {
86
+ for (b in chunk) {
87
+ data.add(b)
88
+ }
89
+ }
90
+
91
+ /** 还没被消费的字节数(调用方用它判断「读完没有」与打日志) */
92
+ public func buffered(): Int64 {
93
+ return Int64(data.size) - cursor
94
+ }
95
+
96
+ /** 断开 / 重连时清干净:残留半条报文会让下一次连接从错位的字节开始解 */
97
+ public func reset(): Unit {
98
+ data = ArrayList<UInt8>()
99
+ cursor = 0
100
+ }
101
+
102
+ /** 取下一个完整值;不够则原地返回 `Incomplete`,协议非法返回 `Malformed`(游标都不动) */
103
+ public func respTryRead(): RespRead {
104
+ let (status, next) = parseValue(cursor, 0)
105
+ match (status) {
106
+ case RespRead.Value(v) =>
107
+ cursor = next
108
+ compact()
109
+ return RespRead.Value(v)
110
+ case RespRead.Malformed(m) =>
111
+ return RespRead.Malformed(m)
112
+ case RespRead.Incomplete =>
113
+ return RespRead.Incomplete
114
+ }
115
+ }
116
+
117
+ /** 已读部分攒够阈值就搬掉,避免长连接下缓冲线性增长 */
118
+ private func compact(): Unit {
119
+ if (cursor < RESP_COMPACT_THRESHOLD) {
120
+ return
121
+ }
122
+ let kept = ArrayList<UInt8>()
123
+ var i = cursor
124
+ while (i < Int64(data.size)) {
125
+ kept.add(data[i])
126
+ i += 1
127
+ }
128
+ data = kept
129
+ cursor = 0
130
+ }
131
+
132
+ /**
133
+ * 从 `off` 处解析一个值,返回 (状态, 下一个位置)。
134
+ * 递归下降;`depth` 超限直接判 Malformed——对端不能用一个深嵌套回复把递归栈打爆。
135
+ */
136
+ private func parseValue(off: Int64, depth: Int64): (RespRead, Int64) {
137
+ if (depth > RESP_MAX_DEPTH) {
138
+ return (RespRead.Malformed("嵌套层数超过上限 ${RESP_MAX_DEPTH}"), off)
139
+ }
140
+ if (off >= Int64(data.size)) {
141
+ return (RespRead.Incomplete, off)
142
+ }
143
+ let marker = data[off]
144
+ if (marker == RESP_SIMPLE || marker == RESP_ERROR) {
145
+ match (findCrlf(off + 1)) {
146
+ case Some(eol) =>
147
+ let text = respSafeText(sliceOf(off + 1, eol))
148
+ let v = if (marker == RESP_SIMPLE) {
149
+ RespValue.Simple(text)
150
+ } else {
151
+ RespValue.Failure(text)
152
+ }
153
+ return (RespRead.Value(v), eol + 2)
154
+ case None =>
155
+ return (RespRead.Incomplete, off)
156
+ }
157
+ }
158
+ if (marker == RESP_INTEGER) {
159
+ match (findCrlf(off + 1)) {
160
+ case Some(eol) =>
161
+ match (respParseInt(respSafeText(sliceOf(off + 1, eol)))) {
162
+ case Some(n) => return (RespRead.Value(RespValue.Number(n)), eol + 2)
163
+ case None => return (RespRead.Malformed("整数不是合法数字"), off)
164
+ }
165
+ case None =>
166
+ return (RespRead.Incomplete, off)
167
+ }
168
+ }
169
+ if (marker == RESP_BULK) {
170
+ match (findCrlf(off + 1)) {
171
+ case Some(eol) =>
172
+ match (parseLength(off + 1, eol)) {
173
+ case Some(-1) => return (RespRead.Value(RespValue.Bulk(None)), eol + 2)
174
+ case Some(n) =>
175
+ // 长度行 + CRLF + n 字节 + CRLF,少一个字节都还在「没收全」
176
+ let bodyStart = eol + 2
177
+ let bodyEnd = bodyStart + n
178
+ if (bodyEnd + 2 > Int64(data.size)) {
179
+ return (RespRead.Incomplete, off)
180
+ }
181
+ if (data[bodyEnd] != 13 || data[bodyEnd + 1] != 10) {
182
+ return (RespRead.Malformed("批量串结尾不是 CRLF"), off)
183
+ }
184
+ return (RespRead.Value(RespValue.Bulk(Some(sliceOf(bodyStart, bodyEnd)))), bodyEnd + 2)
185
+ case None => return (RespRead.Malformed("批量串长度非法"), off)
186
+ }
187
+ case None =>
188
+ return (RespRead.Incomplete, off)
189
+ }
190
+ }
191
+ if (marker == RESP_ARRAY) {
192
+ match (findCrlf(off + 1)) {
193
+ case Some(eol) =>
194
+ match (parseLength(off + 1, eol)) {
195
+ case Some(-1) => return (RespRead.Value(RespValue.Arr(None)), eol + 2)
196
+ case Some(n) =>
197
+ if (n > RESP_MAX_ELEMENTS) {
198
+ return (RespRead.Malformed("数组元素数超过上限 ${RESP_MAX_ELEMENTS}"), off)
199
+ }
200
+ let items = ArrayList<RespValue>()
201
+ var at = eol + 2
202
+ var i: Int64 = 0
203
+ while (i < n) {
204
+ let (subStatus, subNext) = parseValue(at, depth + 1)
205
+ match (subStatus) {
206
+ case RespRead.Value(sv) =>
207
+ items.add(sv)
208
+ at = subNext
209
+ case RespRead.Malformed(m) => return (RespRead.Malformed(m), off)
210
+ case RespRead.Incomplete => return (RespRead.Incomplete, off)
211
+ }
212
+ i += 1
213
+ }
214
+ return (RespRead.Value(RespValue.Arr(Some(items.toArray()))), at)
215
+ case None => return (RespRead.Malformed("数组长度非法"), off)
216
+ }
217
+ case None =>
218
+ return (RespRead.Incomplete, off)
219
+ }
220
+ }
221
+ return (RespRead.Malformed("不认识的 RESP 前缀 0x${respHexByte(marker)}(只做 RESP2)"), off)
222
+ }
223
+
224
+ /** 长度行(批量串 / 数组共用):十进制整数,别的都算非法 */
225
+ private func parseLength(from: Int64, to: Int64): Option<Int64> {
226
+ match (respParseInt(respSafeText(sliceOf(from, to)))) {
227
+ case Some(n) =>
228
+ if (n < -1) {
229
+ return None
230
+ }
231
+ if (n > RESP_MAX_BULK) {
232
+ return None
233
+ }
234
+ return Some(n)
235
+ case None => return None
236
+ }
237
+ }
238
+
239
+ /** 从 `from` 处找 CRLF,返回 CR 的下标;没找着说明这行还没收全 */
240
+ private func findCrlf(from: Int64): Option<Int64> {
241
+ var i = from
242
+ while (i + 1 < Int64(data.size)) {
243
+ if (data[i] == 13 && data[i + 1] == 10) {
244
+ return Some(i)
245
+ }
246
+ i += 1
247
+ }
248
+ return None
249
+ }
250
+
251
+ /** 切一段字节出来(越界自动夹住,调用方都已判过长度) */
252
+ private func sliceOf(from: Int64, to: Int64): Array<UInt8> {
253
+ let out = ArrayList<UInt8>()
254
+ var i = from
255
+ while (i < to && i < Int64(data.size)) {
256
+ out.add(data[i])
257
+ i += 1
258
+ }
259
+ return out.toArray()
260
+ }
261
+ }
262
+
263
+ // ---------------------------------------------------------------- 文本与十六进制
264
+
265
+ /** 一个 nibble(0..15)的 hex 字符字节(大写):48='0'、65='A' */
266
+ func respHexDigit(v: UInt8): UInt8 {
267
+ if (v < 10u8) {
268
+ return 48u8 + v
269
+ }
270
+ return 65u8 + (v - 10u8)
271
+ }
272
+
273
+ /** 一个字节的两位大写 hex */
274
+ public func respHexByte(b: UInt8): String {
275
+ let out = ArrayList<UInt8>()
276
+ out.add(respHexDigit((b >> 4) & 0x0F))
277
+ out.add(respHexDigit(b & 0x0F))
278
+ return String.fromUtf8(out.toArray())
279
+ }
280
+
281
+ /** 一段字节的 hex 串(无分隔符,长度翻倍——调用方自己决定要不要截断) */
282
+ public func respHexOfBytes(raw: Array<UInt8>): String {
283
+ let out = ArrayList<UInt8>()
284
+ var i: Int64 = 0
285
+ while (i < Int64(raw.size)) {
286
+ let b = raw[i]
287
+ out.add(respHexDigit((b >> 4) & 0x0F))
288
+ out.add(respHexDigit(b & 0x0F))
289
+ i += 1
290
+ }
291
+ return String.fromUtf8(out.toArray())
292
+ }
293
+
294
+ /**
295
+ * 把一段字节按文本取出来,取不到就给 hex 兜底。
296
+ *
297
+ * 为什么不直接 `String.fromUtf8`:对端(或中间设备)发坏字节时它会抛,而这段代码正在**解析协议**——
298
+ * 解析路径一律不抛(本文件顶部契约)。全 ASCII 的快路径直接转(绝大多数情况走这里)。
299
+ */
300
+ public func respSafeText(raw: Array<UInt8>): String {
301
+ var ascii = true
302
+ for (b in raw) {
303
+ if (b >= 128u8) {
304
+ ascii = false
305
+ break
306
+ }
307
+ }
308
+ if (ascii) {
309
+ return String.fromUtf8(raw)
310
+ }
311
+ try {
312
+ return String.fromUtf8(raw)
313
+ } catch (e: Exception) {
314
+ return "0x" + respHexOfBytes(raw)
315
+ }
316
+ }
317
+
318
+ /**
319
+ * 十进制整数安全解析(协议解析路径不许抛,所以不直接用 `Int64.parse`)。
320
+ * 只认可选的符号 + 纯数字;超过 2^53 判 None(协议里的长度与计数都不该这么大,判非法比悄悄溢出安全)。
321
+ */
322
+ public func respParseInt(s: String): Option<Int64> {
323
+ let bytes = s.toArray()
324
+ if (bytes.size == 0) {
325
+ return None
326
+ }
327
+ var i: Int64 = 0
328
+ var negative = false
329
+ if (bytes[0] == 45) { // '-'
330
+ negative = true
331
+ i = 1
332
+ } else if (bytes[0] == 43) { // '+'
333
+ i = 1
334
+ }
335
+ if (i >= Int64(bytes.size)) {
336
+ return None
337
+ }
338
+ var value: Int64 = 0
339
+ while (i < Int64(bytes.size)) {
340
+ let b = bytes[i]
341
+ if (b < 48u8 || b > 57u8) {
342
+ return None
343
+ }
344
+ value = value * 10 + Int64(b - 48u8)
345
+ if (value > 9007199254740991) {
346
+ return None
347
+ }
348
+ i += 1
349
+ }
350
+ if (negative) {
351
+ value = -value
352
+ }
353
+ return Some(value)
354
+ }
355
+
356
+ // ---------------------------------------------------------------- 命令组帧
357
+
358
+ /** 追加 ASCII 文本(命令名与十进制长度都是 ASCII) */
359
+ func respAscii(out: ArrayList<UInt8>, s: String): Unit {
360
+ for (b in s.toArray()) {
361
+ out.add(b)
362
+ }
363
+ }
364
+
365
+ func respCrlf(out: ArrayList<UInt8>): Unit {
366
+ out.add(13)
367
+ out.add(10)
368
+ }
369
+
370
+ /**
371
+ * 组一条命令:RESP2 只有数组形式——`*N\r\n` + 每个参数一个 `$len\r\n<字节>\r\n`。
372
+ *
373
+ * 参数个数为 0 直接抛:那是调用方的 bug(对端收到 `*0` 只会回一个协议错误,比当场报错难查)。
374
+ */
375
+ public func respEncodeCommandRaw(parts: Array<Array<UInt8>>): Array<UInt8> {
376
+ if (parts.size == 0) {
377
+ throw CommandException("Redis 命令至少要有一个参数")
378
+ }
379
+ let out = ArrayList<UInt8>()
380
+ out.add(RESP_ARRAY)
381
+ respAscii(out, "${parts.size}")
382
+ respCrlf(out)
383
+ for (p in parts) {
384
+ if (Int64(p.size) > RESP_MAX_BULK) {
385
+ throw CommandException("Redis 参数太长(${p.size} 字节,上限 ${RESP_MAX_BULK})")
386
+ }
387
+ out.add(RESP_BULK)
388
+ respAscii(out, "${p.size}")
389
+ respCrlf(out)
390
+ for (b in p) {
391
+ out.add(b)
392
+ }
393
+ respCrlf(out)
394
+ }
395
+ return out.toArray()
396
+ }
397
+
398
+ /** 文本参数的组帧(`SET key value` 这类) */
399
+ public func respEncodeCommand(parts: Array<String>): Array<UInt8> {
400
+ let raws = ArrayList<Array<UInt8>>()
401
+ for (p in parts) {
402
+ raws.add(p.toArray())
403
+ }
404
+ return respEncodeCommandRaw(raws.toArray())
405
+ }
406
+
407
+ /** 1 / 2 / 3 个文本参数的常用形状(省掉到处写字面量数组) */
408
+ public func respCmd1(a: String): Array<UInt8> {
409
+ return respEncodeCommand([a])
410
+ }
411
+
412
+ public func respCmd2(a: String, b: String): Array<UInt8> {
413
+ return respEncodeCommand([a, b])
414
+ }
415
+
416
+ public func respCmd3(a: String, b: String, c: String): Array<UInt8> {
417
+ return respEncodeCommand([a, b, c])
418
+ }
419
+
420
+ /** 带二进制尾参的形状:`SET key <bytes>` / `XADD key * field <bytes>` —— 值必须二进制安全 */
421
+ public func respCmdRaw2(a: String, b: Array<UInt8>): Array<UInt8> {
422
+ return respEncodeCommandRaw([a.toArray(), b])
423
+ }
424
+
425
+ public func respCmdRaw3(a: String, b: String, c: Array<UInt8>): Array<UInt8> {
426
+ return respEncodeCommandRaw([a.toArray(), b.toArray(), c])
427
+ }
428
+
429
+ // ---------------------------------------------------------------- 取值助手
430
+
431
+ /** 错误回复的文字;不是错误给 None(插件拿它决定要不要抛) */
432
+ public func respErrorOf(v: RespValue): Option<String> {
433
+ match (v) {
434
+ case Failure(m) => return Some(m)
435
+ case _ => return None
436
+ }
437
+ }
438
+
439
+ /** 整数回复(`:` 形式) */
440
+ public func respNumber(v: RespValue): Option<Int64> {
441
+ match (v) {
442
+ case Number(n) => return Some(n)
443
+ case _ => return None
444
+ }
445
+ }
446
+
447
+ /** 批量串的原始字节。**不收** Simple / Number:它们不是 bulk,混用会让调用方以为拿到了二进制 */
448
+ public func respBytes(v: RespValue): Option<Array<UInt8>> {
449
+ match (v) {
450
+ case Bulk(Some(raw)) => return Some(raw)
451
+ case _ => return None
452
+ }
453
+ }
454
+
455
+ /** 批量串当文本(坏字节 → hex 兜底) */
456
+ public func respText(v: RespValue): Option<String> {
457
+ match (v) {
458
+ case Bulk(Some(raw)) => return Some(respSafeText(raw))
459
+ case _ => return None
460
+ }
461
+ }
462
+
463
+ /** 数组元素;null 数组(`*-1`)给 None */
464
+ public func respItems(v: RespValue): Option<Array<RespValue>> {
465
+ match (v) {
466
+ case Arr(Some(items)) => return Some(items)
467
+ case _ => return None
468
+ }
469
+ }
470
+
471
+ /** 数组元素,null / 非数组都当空数组——查询类命令的调用方几乎总是这个语义 */
472
+ public func respItemsOrEmpty(v: RespValue): Array<RespValue> {
473
+ match (v) {
474
+ case Arr(Some(items)) => return items
475
+ case _ => return Array<RespValue>()
476
+ }
477
+ }
478
+
479
+ /**
480
+ * 扁平数组当「文本对」列表:`HGETALL` 与 `ZRANGE ... WITHSCORES` 都是 `[a, b, c, d]` 的形状。
481
+ * 奇数个元素就丢掉最后一个(回复本身已经不对了,但没必要为它抛)。
482
+ */
483
+ public func respPairs(v: RespValue): Array<(String, String)> {
484
+ let items = respItemsOrEmpty(v)
485
+ let out = ArrayList<(String, String)>()
486
+ var i: Int64 = 0
487
+ while (i + 1 < Int64(items.size)) {
488
+ out.add((respValueText(items[i]), respValueText(items[i + 1])))
489
+ i += 2
490
+ }
491
+ return out.toArray()
492
+ }
493
+
494
+ /** 单个值当文本:Simple 与 Number 也收(键名 / TTL / 分值常以这两种形式回来) */
495
+ public func respValueText(v: RespValue): String {
496
+ match (v) {
497
+ case Simple(s) => return s
498
+ case Number(n) => return n.toString()
499
+ case Bulk(Some(raw)) => return respSafeText(raw)
500
+ case Bulk(None) => return ""
501
+ case Failure(m) => return m
502
+ case Arr(None) => return ""
503
+ case Arr(Some(items)) => return "${items.size} items"
504
+ }
505
+ }
506
+
507
+ /**
508
+ * 一行摘要:**只说类型与规模,不打内容**——stderr 取证行要短,把整条 INFO 打出来会把日志淹掉。
509
+ * 这是「日志里有没有」之外还能一眼看出「收了多少」的那个口径。
510
+ */
511
+ public func respBrief(v: RespValue): String {
512
+ match (v) {
513
+ case Simple(s) => return "simple(${s})"
514
+ case Failure(m) => return "error(${m})"
515
+ case Number(n) => return "number(${n})"
516
+ case Bulk(Some(raw)) => return "bulk(${raw.size}B)"
517
+ case Bulk(None) => return "null"
518
+ case Arr(Some(items)) => return "array(${items.size})"
519
+ case Arr(None) => return "null"
520
+ }
521
+ }
522
+
523
+ /**
524
+ * 人看的完整渲染(前端展示与单测断言用):数组按缩排多行,二进制走 hex。
525
+ * 不用 JSON 序列化:这里的值可能是非 UTF-8 字节,JSON 化要在中途转一次文本,反而丢掉「这是二进制」这件事。
526
+ */
527
+ public func respRender(v: RespValue): String {
528
+ return respRenderAt(v, 0)
529
+ }
530
+
531
+ func respRenderAt(v: RespValue, depth: Int64): String {
532
+ match (v) {
533
+ case Simple(s) => return s
534
+ case Failure(m) => return "(error) ${m}"
535
+ case Number(n) => return "(integer) ${n}"
536
+ case Bulk(None) => return "(nil)"
537
+ case Bulk(Some(raw)) => return "(bulk ${raw.size}B) ${respSafeText(raw)}"
538
+ case Arr(None) => return "(nil array)"
539
+ case Arr(Some(items)) =>
540
+ let out = ArrayList<UInt8>()
541
+ respAscii(out, "(array ${items.size})")
542
+ var i: Int64 = 0
543
+ while (i < Int64(items.size)) {
544
+ out.add(10u8) // '\n'
545
+ var k: Int64 = 0
546
+ while (k < (depth + 1) * 2) {
547
+ out.add(32u8)
548
+ k += 1
549
+ }
550
+ respAscii(out, "${i}) ")
551
+ respAscii(out, respRenderAt(items[i], depth + 1))
552
+ i += 1
553
+ }
554
+ return String.fromUtf8(out.toArray())
555
+ }
556
+ }