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,40 @@
|
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.ws.*
|
|
5
|
+
import std.unittest.*
|
|
6
|
+
import std.unittest.testmacro.*
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* ws 插件的策略层用例(帧 IO 归实机:真连接需要对端,单测不起网络)。
|
|
10
|
+
* urlHostOf 的 ws/wss 用例也放这里(核心共享层函数,随 ws 插件落地一起验)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
func testUrlHostOfAcceptsWsSchemes(): Unit {
|
|
15
|
+
// cjh web 的默认端点形状:ws://127.0.0.1:8765/ws
|
|
16
|
+
@Expect(urlHostOf("ws://127.0.0.1:8765/ws"), "127.0.0.1")
|
|
17
|
+
@Expect(urlHostOf("wss://Example.COM/ws"), "example.com")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
func testWsPluginDeclarations(): Unit {
|
|
22
|
+
let p = WsPlugin()
|
|
23
|
+
@Expect(p.name(), "ws")
|
|
24
|
+
@Expect(p.scopeAware(), true)
|
|
25
|
+
// 权限集 default = connect + send + close:集声明 != 放行,清单引用才生效
|
|
26
|
+
@Expect(p.permissions().contains("default"), true)
|
|
27
|
+
let cmds = p.commands()
|
|
28
|
+
@Expect(cmds.contains("connect"), true)
|
|
29
|
+
@Expect(cmds.contains("send"), true)
|
|
30
|
+
@Expect(cmds.contains("close"), true)
|
|
31
|
+
// 推两个事件(frame / state),无前端 shim
|
|
32
|
+
@Expect(p.events().size == 2, true)
|
|
33
|
+
@Expect(p.jsShim().size == 0, true)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
func testWsStateConstants(): Unit {
|
|
38
|
+
@Expect(CJ_WS_STATE_CONNECTED, "connected")
|
|
39
|
+
@Expect(CJ_WS_STATE_CLOSED, "closed")
|
|
40
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import std.unittest.*
|
|
5
|
+
import std.unittest.testmacro.*
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* 静默启动的参数线格式(`joinHiddenArgs`)单测。
|
|
9
|
+
*
|
|
10
|
+
* 真正难的**不是拼接**,是「拼出来还能原样切开」这条不变量:C 桥按 0x1F 把这条串切成 argv,
|
|
11
|
+
* 一旦某个参数自身带着 0x1F,它就在桥那边变成两个参数——而调用方拿到的是「启动成功」,
|
|
12
|
+
* 错误要到目标进程抱怨「未知选项」时才现身。所以这里有两条硬要求:
|
|
13
|
+
* ① 参数**逐字**进线格式(空格 / 引号 / 中文都不在这里转义,转义是平台侧的事);
|
|
14
|
+
* ② 参数里出现 0x1F 就当场拒绝,不许拼接。
|
|
15
|
+
*
|
|
16
|
+
* 起真进程 / 判可见窗口的部分不在这里(需要桌面与平台桥),归实机取证。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** 线格式分隔符:0x1F(单元分隔符),与 C 桥 `CJ_ARG_SEP` 同值 */
|
|
20
|
+
let hiddenSep: String = "\u{1F}"
|
|
21
|
+
|
|
22
|
+
/** 拼装成功取回线格式;被拒绝时回一个可比较的哨兵值(避免在断言里直接比 Option) */
|
|
23
|
+
func hiddenWireOf(args: Array<String>): String {
|
|
24
|
+
let parsed = joinHiddenArgs(args)
|
|
25
|
+
var wire = "<rejected>"
|
|
26
|
+
match (parsed) {
|
|
27
|
+
case Some(w) => wire = w
|
|
28
|
+
case None => wire = "<rejected>"
|
|
29
|
+
}
|
|
30
|
+
return wire
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 是否被拒绝(0x1F 出现在参数里) */
|
|
34
|
+
func hiddenArgsRejected(args: Array<String>): Bool {
|
|
35
|
+
let parsed = joinHiddenArgs(args)
|
|
36
|
+
var rejected = false
|
|
37
|
+
match (parsed) {
|
|
38
|
+
case Some(_) => rejected = false
|
|
39
|
+
case None => rejected = true
|
|
40
|
+
}
|
|
41
|
+
return rejected
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
func testJoinHiddenArgsKeepsOrderAndUsesUnitSeparator(): Unit {
|
|
46
|
+
// 顺序必须与传入一致(桥按位取 argv),分隔符只有一个 0x1F
|
|
47
|
+
let wire = hiddenWireOf(["web", "--port", "8765", "--bind", "127.0.0.1"])
|
|
48
|
+
@Expect(wire, "web" + hiddenSep + "--port" + hiddenSep + "8765" + hiddenSep + "--bind" +
|
|
49
|
+
hiddenSep + "127.0.0.1")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
func testJoinHiddenArgsSingleArgHasNoSeparator(): Unit {
|
|
54
|
+
// 只有一个参数时线格式里不该留下尾随分隔符(否则桥会切出一个空 argv)
|
|
55
|
+
let wire = hiddenWireOf(["web"])
|
|
56
|
+
@Expect(wire, "web")
|
|
57
|
+
// indexOf 回 Option<Int64>,这里只关心「有没有」,拆开再断言(@Expect 对不同泛型的 Option 实参推不出类型)
|
|
58
|
+
let found = wire.indexOf(hiddenSep)
|
|
59
|
+
var hasSep = true
|
|
60
|
+
match (found) {
|
|
61
|
+
case Some(_) => hasSep = true
|
|
62
|
+
case None => hasSep = false
|
|
63
|
+
}
|
|
64
|
+
@Expect(hasSep, false)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
func testJoinHiddenArgsEmptyArgListIsEmptyWire(): Unit {
|
|
69
|
+
// 没参数的启动(如 cjh 只用默认端口)走这条:空线格式,桥侧切成零个 argv
|
|
70
|
+
let wire = hiddenWireOf([])
|
|
71
|
+
@Expect(wire, "")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
func testJoinHiddenArgsKeepsSpacesQuotesAndChineseVerbatim(): Unit {
|
|
76
|
+
// 参数原样上线:不在这里加引号、不转义——那些是平台侧(CreateProcessW / posix_spawn)的事。
|
|
77
|
+
// 中文按 UTF-8 逐字节过桥,不做任何改写。
|
|
78
|
+
let wire = hiddenWireOf(["hello world", "\"quoted\"", "中文参数", "--path=C:/a b/c"])
|
|
79
|
+
@Expect(wire, "hello world" + hiddenSep + "\"quoted\"" + hiddenSep + "中文参数" + hiddenSep +
|
|
80
|
+
"--path=C:/a b/c")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
func testJoinHiddenArgsKeepsEmptyArgSlot(): Unit {
|
|
85
|
+
// 中间那个空串参数不能被吞:它占一个 argv 位,桥那边必须收到一个空参数
|
|
86
|
+
let wire = hiddenWireOf(["a", "", "b"])
|
|
87
|
+
@Expect(wire, "a" + hiddenSep + hiddenSep + "b")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test
|
|
91
|
+
func testJoinHiddenArgsRoundTripsThroughSplit(): Unit {
|
|
92
|
+
// 不变量:拼出来还能按同一个分隔符切回去,条数不变、逐条相等
|
|
93
|
+
let args = ["web", "--port", "8765", "--bind", "127.0.0.1"]
|
|
94
|
+
let back = hiddenWireOf(args).split(hiddenSep)
|
|
95
|
+
@Expect(back.size, args.size)
|
|
96
|
+
var same = true
|
|
97
|
+
for (i in 0..args.size) {
|
|
98
|
+
if (back[i] != args[i]) {
|
|
99
|
+
same = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
@Expect(same, true)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
func testJoinHiddenArgsRejectsUnitSeparatorInArg(): Unit {
|
|
107
|
+
// 0x1F 混在参数里 → 必须拒绝(拼接成功等于把参数悄悄劈成两个)
|
|
108
|
+
@Expect(hiddenArgsRejected(["plain" + hiddenSep + "injected"]), true)
|
|
109
|
+
// 位置无关:开头 / 中间 / 末尾都要拒
|
|
110
|
+
@Expect(hiddenArgsRejected([hiddenSep + "lead"]), true)
|
|
111
|
+
@Expect(hiddenArgsRejected(["--port", "1" + hiddenSep + "2"]), true)
|
|
112
|
+
@Expect(hiddenArgsRejected(["trail" + hiddenSep]), true)
|
|
113
|
+
// 整个参数就是一个分隔符
|
|
114
|
+
@Expect(hiddenArgsRejected([hiddenSep]), true)
|
|
115
|
+
// 对照:正常参数不被拒(否则上面的「拒绝」可能只是恒 true)
|
|
116
|
+
@Expect(hiddenArgsRejected(["web", "--port", "8765"]), false)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
func testLaunchHiddenRejectsEmptyPathWithoutTouchingBridge(): Unit {
|
|
121
|
+
// 空路径在**进桥之前**就返回 None:不需要任何平台产物,也不会留下 pid
|
|
122
|
+
let started = launchHidden("", ["web"])
|
|
123
|
+
var isNone = false
|
|
124
|
+
match (started) {
|
|
125
|
+
case Some(_) => isNone = false
|
|
126
|
+
case None => isNone = true
|
|
127
|
+
}
|
|
128
|
+
@Expect(isNone, true)
|
|
129
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.redis.*
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.unittest.*
|
|
7
|
+
import std.unittest.testmacro.*
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* Redis RESP2 编解码单元测试(锁 src/plugins/redis_codec.cj 的纯函数)。
|
|
11
|
+
*
|
|
12
|
+
* 锁四件事:① 命令组帧的**逐字节线格式**(长度按字节、二进制安全、空命令即拒);
|
|
13
|
+
* ② 增量解析的三态语义——「还没收全」必须原地不动,「协议非法」必须当场拒,且两者不能混;
|
|
14
|
+
* ③ 畸形输入一律不抛异常(未知前缀 / 长度非数字 / 结尾不是 CRLF / 深嵌套 / 谎报长度);
|
|
15
|
+
* ④ 取值助手(null 与空串能分开、扁平数组能当键值对、二进制有 hex 兜底)。
|
|
16
|
+
*
|
|
17
|
+
* 期望值一律写成十六进制字符串:CRLF 与长度字段一眼可见——这类地方错一个字节,对端只会「不响应」。
|
|
18
|
+
* 注意 `@Expect` 对 enum 实参推不出泛型(AGENTS §4),所以断言一律走取值助手 + 布尔/字符串比较。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** 十六进制文本 → 字节(测试里内联构造线上报文;不合法直接让用例失败) */
|
|
22
|
+
func redisHex(hex: String): Array<UInt8> {
|
|
23
|
+
return hexToBytes(hex).getOrThrow()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `Option` 是不是 None(不用 isNone():不想在单测里引入没在本仓出现过的 API) */
|
|
27
|
+
func redisIsNone<T>(v: Option<T>): Bool {
|
|
28
|
+
match (v) {
|
|
29
|
+
case None => return true
|
|
30
|
+
case _ => return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 喂一段字节并取一个值;顺便把「三态」拆成好断言的形式 */
|
|
35
|
+
func redisReadOne(hex: String): RespRead {
|
|
36
|
+
let reader = RespReader()
|
|
37
|
+
reader.feed(redisHex(hex))
|
|
38
|
+
return reader.respTryRead()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 读取结果的分类名(断言里比字符串更直观,也避开 enum 实参的泛型推断问题) */
|
|
42
|
+
func redisReadKind(r: RespRead): String {
|
|
43
|
+
match (r) {
|
|
44
|
+
case RespRead.Incomplete => return "incomplete"
|
|
45
|
+
case RespRead.Value(_) => return "value"
|
|
46
|
+
case RespRead.Malformed(m) => return "malformed"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 读取结果里的值;不是 Value 就给一个空 simple(用例会先断言 kind,这里只做搬运) */
|
|
51
|
+
func redisReadValue(r: RespRead): RespValue {
|
|
52
|
+
match (r) {
|
|
53
|
+
case RespRead.Value(v) => return v
|
|
54
|
+
case _ => return RespValue.Simple("<not-a-value>")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Malformed 的说明文字;不是 Malformed 就给空串 */
|
|
59
|
+
func redisReadMessage(r: RespRead): String {
|
|
60
|
+
match (r) {
|
|
61
|
+
case RespRead.Malformed(m) => return m
|
|
62
|
+
case _ => return ""
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------- 命令组帧
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
func testEncodeCommandWireFormat(): Unit {
|
|
70
|
+
// *3 CRLF $3 SET CRLF $3 key CRLF $5 value CRLF
|
|
71
|
+
@Expect(bytesToHex(respEncodeCommand(["SET", "key", "value"])),
|
|
72
|
+
"2a330d0a24330d0a5345540d0a24330d0a6b65790d0a24350d0a76616c75650d0a")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
func testEncodeCommandLengthCountsBytesNotRunes(): Unit {
|
|
77
|
+
// 「键」「值」各 3 字节(UTF-8):长度字段写 3 而不是 1——按码点算会让对端解析错位
|
|
78
|
+
@Expect(bytesToHex(respEncodeCommand(["SET", "键", "值"])),
|
|
79
|
+
"2a330d0a24330d0a5345540d0a24330d0ae994ae0d0a24330d0ae580bc0d0a")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
func testEncodeCommandBinarySafe(): Unit {
|
|
84
|
+
// 值里带 NUL 与 0xFF:长度按字节算,内容原样穿过(调试工具会拿它看二进制 value)
|
|
85
|
+
@Expect(bytesToHex(respCmdRaw3("SET", "blob", redisHex("0001fffe"))),
|
|
86
|
+
"2a330d0a24330d0a5345540d0a24340d0a626c6f620d0a24340d0a0001fffe0d0a")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
func testEncodeCommandBuilders(): Unit {
|
|
91
|
+
@Expect(bytesToHex(respCmd1("PING")), "2a310d0a24340d0a50494e470d0a")
|
|
92
|
+
@Expect(bytesToHex(respCmd2("GET", "user:1")), "2a320d0a24330d0a4745540d0a24360d0a757365723a310d0a")
|
|
93
|
+
@Expect(bytesToHex(respCmdRaw2("GET", redisHex("00ff"))), "2a320d0a24330d0a4745540d0a24320d0a00ff0d0a")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
func testEncodeCommandRejectsEmpty(): Unit {
|
|
98
|
+
// *0 是非法命令(对端只会回一个协议错误,比当场报错难查),所以组帧层直接拒
|
|
99
|
+
var threw = false
|
|
100
|
+
try {
|
|
101
|
+
respEncodeCommand(Array<String>())
|
|
102
|
+
} catch (e: CommandException) {
|
|
103
|
+
threw = true
|
|
104
|
+
@Expect(e.message.contains("至少要有一个参数"), true)
|
|
105
|
+
}
|
|
106
|
+
@Expect(threw, true)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------- 增量解析:基本类型
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
func testParseSimpleAndError(): Unit {
|
|
113
|
+
let ok = redisReadValue(redisReadOne("2b4f4b0d0a")) // +OK
|
|
114
|
+
@Expect(redisReadKind(redisReadOne("2b4f4b0d0a")), "value")
|
|
115
|
+
match (ok) {
|
|
116
|
+
case Simple(s) => @Expect(s, "OK")
|
|
117
|
+
case _ => @Expect("simple 期望", "别拿到了")
|
|
118
|
+
}
|
|
119
|
+
let err = redisReadValue(redisReadOne("2d455252206261640d0a")) // -ERR bad
|
|
120
|
+
match (err) {
|
|
121
|
+
case Failure(m) =>
|
|
122
|
+
@Expect(m, "ERR bad")
|
|
123
|
+
@Expect(respErrorOf(err).getOrThrow(), "ERR bad")
|
|
124
|
+
@Expect(redisIsNone(respNumber(err)), true)
|
|
125
|
+
case _ => @Expect("failure 期望", "别拿到了")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
func testParseNumber(): Unit {
|
|
131
|
+
let v = redisReadValue(redisReadOne("3a3132330d0a")) // :123
|
|
132
|
+
@Expect(respNumber(v).getOrThrow(), 123)
|
|
133
|
+
@Expect(redisIsNone(respBytes(v)), true) // 整数不是 bulk
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
func testParseNullBulkAndNullArray(): Unit {
|
|
138
|
+
// $-1 与 *-1:都是 null,但类型不同;null 必须与「空串」分得开
|
|
139
|
+
let nullBulk = redisReadValue(redisReadOne("242d310d0a"))
|
|
140
|
+
@Expect(redisReadKind(redisReadOne("242d310d0a")), "value")
|
|
141
|
+
@Expect(redisIsNone(respBytes(nullBulk)), true)
|
|
142
|
+
@Expect(respBrief(nullBulk), "null")
|
|
143
|
+
let nullArr = redisReadValue(redisReadOne("2a2d310d0a"))
|
|
144
|
+
@Expect(redisIsNone(respItems(nullArr)), true)
|
|
145
|
+
@Expect(respBrief(nullArr), "null")
|
|
146
|
+
// 空串是 0 字节 bulk(尾部 CRLF 仍要在):Some(空数组),不是 null
|
|
147
|
+
let emptyBulk = redisReadValue(redisReadOne("24300d0a0d0a"))
|
|
148
|
+
@Expect(respBytes(emptyBulk).getOrThrow().size, 0)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Test
|
|
152
|
+
func testParseBulkBinary(): Unit {
|
|
153
|
+
// $4 00 01 FF FE CRLF:二进制值原样出来(不是 UTF-8 就得能拿到字节)
|
|
154
|
+
let v = redisReadValue(redisReadOne("24340d0a0001fffe0d0a"))
|
|
155
|
+
@Expect(respBrief(v), "bulk(4B)")
|
|
156
|
+
@Expect(respHexOfBytes(respBytes(v).getOrThrow()), "0001FFFE")
|
|
157
|
+
@Expect(respSafeText(respBytes(v).getOrThrow()), "0x0001FFFE")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
func testParseNestedArray(): Unit {
|
|
162
|
+
// *2 CRLF *2 CRLF :1 CRLF :2 CRLF +OK CRLF
|
|
163
|
+
let v = redisReadValue(redisReadOne("2a320d0a2a320d0a3a310d0a3a320d0a2b4f4b0d0a"))
|
|
164
|
+
@Expect(respBrief(v), "array(2)")
|
|
165
|
+
let items = respItemsOrEmpty(v)
|
|
166
|
+
@Expect(items.size, 2)
|
|
167
|
+
@Expect(respBrief(items[0]), "array(2)")
|
|
168
|
+
@Expect(respNumber(respItemsOrEmpty(items[0])[0]).getOrThrow(), 1)
|
|
169
|
+
@Expect(respNumber(respItemsOrEmpty(items[0])[1]).getOrThrow(), 2)
|
|
170
|
+
match (items[1]) {
|
|
171
|
+
case Simple(s) => @Expect(s, "OK")
|
|
172
|
+
case _ => @Expect("尾部 simple 期望", "别拿到了")
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
func testParseFlatArrayAsPairs(): Unit {
|
|
178
|
+
// HGETALL 的形状:*4 name alice city Beijing → 两对
|
|
179
|
+
let v = redisReadValue(redisReadOne(
|
|
180
|
+
"2a340d0a24340d0a6e616d650d0a24350d0a616c6963650d0a24340d0a636974790d0a24370d0a4265696a696e670d0a"))
|
|
181
|
+
let pairs = respPairs(v)
|
|
182
|
+
@Expect(pairs.size, 2)
|
|
183
|
+
@Expect(pairs[0][0], "name")
|
|
184
|
+
@Expect(pairs[0][1], "alice")
|
|
185
|
+
@Expect(pairs[1][0], "city")
|
|
186
|
+
@Expect(pairs[1][1], "Beijing")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------- 增量解析:跨块与游标
|
|
190
|
+
|
|
191
|
+
@Test
|
|
192
|
+
func testIncrementalAcrossChunks(): Unit {
|
|
193
|
+
// 一条回复跨两个 socket 块到达:第一块必须判「没收全」且不动游标,第二块补齐后才出值
|
|
194
|
+
let reader = RespReader()
|
|
195
|
+
reader.feed(redisHex("2a320d0a24330d0a666f6f")) // *2 $3 foo(还差 CRLF 与第二个参数)
|
|
196
|
+
@Expect(redisReadKind(reader.respTryRead()), "incomplete")
|
|
197
|
+
@Expect(reader.buffered(), 11)
|
|
198
|
+
reader.feed(redisHex("0d0a24330d0a6261720d0a")) // CRLF $3 bar CRLF
|
|
199
|
+
let v = redisReadValue(reader.respTryRead())
|
|
200
|
+
@Expect(respBrief(v), "array(2)")
|
|
201
|
+
let items = respItemsOrEmpty(v)
|
|
202
|
+
@Expect(respValueText(items[0]), "foo")
|
|
203
|
+
@Expect(respValueText(items[1]), "bar")
|
|
204
|
+
// 读干净了:缓冲见底,再读还是「没收全」(而不是把旧值再吐一遍)
|
|
205
|
+
@Expect(reader.buffered(), 0)
|
|
206
|
+
@Expect(redisReadKind(reader.respTryRead()), "incomplete")
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@Test
|
|
210
|
+
func testTwoValuesInOneBuffer(): Unit {
|
|
211
|
+
// 粘包:一次读到两条回复,必须一条一条吐,且顺序不变
|
|
212
|
+
let reader = RespReader()
|
|
213
|
+
reader.feed(redisHex("2b4f4b0d0a3a370d0a"))
|
|
214
|
+
match (redisReadValue(reader.respTryRead())) {
|
|
215
|
+
case Simple(s) => @Expect(s, "OK")
|
|
216
|
+
case _ => @Expect("第一条应为 simple", "别拿到了")
|
|
217
|
+
}
|
|
218
|
+
@Expect(respNumber(redisReadValue(reader.respTryRead())).getOrThrow(), 7)
|
|
219
|
+
@Expect(redisReadKind(reader.respTryRead()), "incomplete")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@Test
|
|
223
|
+
func testResetClearsPendingBytes(): Unit {
|
|
224
|
+
// 断开重连时必须能清干净:残留半条报文会让下一次连接从错位字节开始解
|
|
225
|
+
let reader = RespReader()
|
|
226
|
+
reader.feed(redisHex("24330d0a61"))
|
|
227
|
+
@Expect(reader.buffered(), 5)
|
|
228
|
+
reader.reset()
|
|
229
|
+
@Expect(reader.buffered(), 0)
|
|
230
|
+
reader.feed(redisHex("2b4f4b0d0a"))
|
|
231
|
+
match (redisReadValue(reader.respTryRead())) {
|
|
232
|
+
case Simple(s) => @Expect(s, "OK")
|
|
233
|
+
case _ => @Expect("reset 后应能正常读", "别拿到了")
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------- 畸形输入:一律 Malformed,不抛
|
|
238
|
+
|
|
239
|
+
@Test
|
|
240
|
+
func testMalformedUnknownPrefix(): Unit {
|
|
241
|
+
// '%'(RESP3 map)与 '>'(RESP3 push)都在边界外:不猜它想表达什么,直接拒
|
|
242
|
+
let map = redisReadOne("25320d0a")
|
|
243
|
+
@Expect(redisReadKind(map), "malformed")
|
|
244
|
+
@Expect(redisReadMessage(map).contains("只做 RESP2"), true)
|
|
245
|
+
let push = redisReadOne("3e0d0a")
|
|
246
|
+
@Expect(redisReadKind(push), "malformed")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@Test
|
|
250
|
+
func testMalformedBadInteger(): Unit {
|
|
251
|
+
let r = redisReadOne("3a3132610d0a") // :12a
|
|
252
|
+
@Expect(redisReadKind(r), "malformed")
|
|
253
|
+
@Expect(redisReadMessage(r).contains("整数不是合法数字"), true)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@Test
|
|
257
|
+
func testMalformedBulkLengthNotNumber(): Unit {
|
|
258
|
+
let r = redisReadOne("246162630d0a") // $abc
|
|
259
|
+
@Expect(redisReadKind(r), "malformed")
|
|
260
|
+
@Expect(redisReadMessage(r).contains("批量串长度非法"), true)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@Test
|
|
264
|
+
func testMalformedBulkLengthBeyondCap(): Unit {
|
|
265
|
+
// 谎报 1e16 字节:按协议上限拒(也顺带盖住「整数超 2^53 判非法」那条路),而不是等它把内存吃干
|
|
266
|
+
let r = redisReadOne("2431303030303030303030303030303030300d0a")
|
|
267
|
+
@Expect(redisReadKind(r), "malformed")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Test
|
|
271
|
+
func testMalformedBulkMissingTrailingCrlf(): Unit {
|
|
272
|
+
// 长度声明 3、后面有 3 字节但收尾不是 CRLF:判非法,而不是「再等等」(否则会一直等下去)
|
|
273
|
+
let r = redisReadOne("24330d0a6162635858")
|
|
274
|
+
@Expect(redisReadKind(r), "malformed")
|
|
275
|
+
@Expect(redisReadMessage(r).contains("CRLF"), true)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@Test
|
|
279
|
+
func testMalformedNegativeArrayLength(): Unit {
|
|
280
|
+
// *-2:只有 *-1 是合法的 null 数组
|
|
281
|
+
@Expect(redisReadKind(redisReadOne("2a2d320d0a")), "malformed")
|
|
282
|
+
@Expect(redisReadKind(redisReadOne("2a6162630d0a")), "malformed") // *abc
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@Test
|
|
286
|
+
func testMalformedDepthLimit(): Unit {
|
|
287
|
+
// 40 层嵌套(每层 *1):超上限即判非法——对端不能用一个回复把递归栈打爆
|
|
288
|
+
let raw = ArrayList<UInt8>()
|
|
289
|
+
var i: Int64 = 0
|
|
290
|
+
while (i < 40) {
|
|
291
|
+
for (b in redisHex("2a310d0a")) {
|
|
292
|
+
raw.add(b)
|
|
293
|
+
}
|
|
294
|
+
i += 1
|
|
295
|
+
}
|
|
296
|
+
for (b in redisHex("2b4f4b0d0a")) {
|
|
297
|
+
raw.add(b)
|
|
298
|
+
}
|
|
299
|
+
let reader = RespReader()
|
|
300
|
+
reader.feed(raw.toArray())
|
|
301
|
+
let r = reader.respTryRead()
|
|
302
|
+
@Expect(redisReadKind(r), "malformed")
|
|
303
|
+
@Expect(redisReadMessage(r).contains("嵌套层数超过上限"), true)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@Test
|
|
307
|
+
func testMalformedDoesNotAdvanceCursor(): Unit {
|
|
308
|
+
// 非法输入不消费字节:同一读取器再读还是同一条 malformed(上层报一次错就断开,不必自己清)
|
|
309
|
+
let reader = RespReader()
|
|
310
|
+
reader.feed(redisHex("25310d0a"))
|
|
311
|
+
@Expect(redisReadKind(reader.respTryRead()), "malformed")
|
|
312
|
+
@Expect(redisReadKind(reader.respTryRead()), "malformed")
|
|
313
|
+
@Expect(reader.buffered(), 4)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@Test
|
|
317
|
+
func testIncompleteKeepsEveryByte(): Unit {
|
|
318
|
+
// 半条批量串:一个字节都不能丢(丢了就会从错位处继续解析,后面全乱)
|
|
319
|
+
let reader = RespReader()
|
|
320
|
+
reader.feed(redisHex("24350d0a6865"))
|
|
321
|
+
@Expect(reader.buffered(), 6)
|
|
322
|
+
@Expect(redisReadKind(reader.respTryRead()), "incomplete")
|
|
323
|
+
@Expect(reader.buffered(), 6)
|
|
324
|
+
reader.feed(redisHex("6c6c6f0d0a"))
|
|
325
|
+
@Expect(respValueText(redisReadValue(reader.respTryRead())), "hello")
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------- 取值助手
|
|
329
|
+
|
|
330
|
+
@Test
|
|
331
|
+
func testParseIntHelper(): Unit {
|
|
332
|
+
@Expect(respParseInt("0").getOrThrow(), 0)
|
|
333
|
+
@Expect(respParseInt("123").getOrThrow(), 123)
|
|
334
|
+
@Expect(respParseInt("-1").getOrThrow(), -1)
|
|
335
|
+
@Expect(respParseInt("+7").getOrThrow(), 7)
|
|
336
|
+
@Expect(redisIsNone(respParseInt("")), true)
|
|
337
|
+
@Expect(redisIsNone(respParseInt("-")), true)
|
|
338
|
+
@Expect(redisIsNone(respParseInt("12a")), true)
|
|
339
|
+
@Expect(redisIsNone(respParseInt("1.5")), true)
|
|
340
|
+
@Expect(redisIsNone(respParseInt("10000000000000000")), true) // 超 2^53 判非法
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@Test
|
|
344
|
+
func testHexHelpers(): Unit {
|
|
345
|
+
@Expect(respHexByte(0), "00")
|
|
346
|
+
@Expect(respHexByte(255), "FF")
|
|
347
|
+
@Expect(respHexByte(171), "AB")
|
|
348
|
+
@Expect(respHexOfBytes(redisHex("00ff10")), "00FF10")
|
|
349
|
+
@Expect(respHexOfBytes(Array<UInt8>()), "")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@Test
|
|
353
|
+
func testSafeTextFallback(): Unit {
|
|
354
|
+
@Expect(respSafeText("OK".toArray()), "OK")
|
|
355
|
+
@Expect(respSafeText(Array<UInt8>()), "")
|
|
356
|
+
@Expect(respSafeText(redisHex("ff")), "0xFF") // 坏字节 → hex 兜底
|
|
357
|
+
@Expect(respSafeText(redisHex("e994ae")), "键") // 合法 UTF-8 照常转
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Test
|
|
361
|
+
func testBriefSummaries(): Unit {
|
|
362
|
+
// 取证行用的摘要:只说类型与规模,不打内容(把整条 INFO 打进日志会把凭证淹掉)
|
|
363
|
+
@Expect(respBrief(RespValue.Simple("OK")), "simple(OK)")
|
|
364
|
+
@Expect(respBrief(RespValue.Failure("ERR x")), "error(ERR x)")
|
|
365
|
+
@Expect(respBrief(RespValue.Number(-2)), "number(-2)")
|
|
366
|
+
@Expect(respBrief(RespValue.Bulk(Some(redisHex("6162")))), "bulk(2B)")
|
|
367
|
+
@Expect(respBrief(RespValue.Bulk(Option<Array<UInt8>>.None)), "null")
|
|
368
|
+
@Expect(respBrief(RespValue.Arr(Option<Array<RespValue>>.None)), "null")
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@Test
|
|
372
|
+
func testRenderArray(): Unit {
|
|
373
|
+
// 人看的渲染:数组缩排多行,二进制走 hex(页面里也是这个口径)
|
|
374
|
+
let v = redisReadValue(redisReadOne("2a320d0a24310d0a610d0a24310d0a620d0a"))
|
|
375
|
+
let text = respRender(v)
|
|
376
|
+
@Expect(text.contains("(array 2)"), true)
|
|
377
|
+
@Expect(text.contains("0) (bulk 1B) a"), true)
|
|
378
|
+
@Expect(text.contains("1) (bulk 1B) b"), true)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@Test
|
|
382
|
+
func testValueTextShapes(): Unit {
|
|
383
|
+
// 键名 / TTL / 分值常以 simple 或整数形式回来:取值不能只认 bulk
|
|
384
|
+
@Expect(respValueText(RespValue.Simple("OK")), "OK")
|
|
385
|
+
@Expect(respValueText(RespValue.Number(-2)), "-2")
|
|
386
|
+
@Expect(respValueText(RespValue.Bulk(Option<Array<UInt8>>.None)), "")
|
|
387
|
+
@Expect(respValueText(redisReadValue(redisReadOne("2a320d0a3a310d0a3a320d0a"))), "2 items")
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@Test
|
|
391
|
+
func testItemsOrEmptyToleratesNonArray(): Unit {
|
|
392
|
+
@Expect(respItemsOrEmpty(RespValue.Simple("OK")).size, 0)
|
|
393
|
+
@Expect(redisIsNone(respItems(RespValue.Simple("OK"))), true)
|
|
394
|
+
@Expect(respPairs(RespValue.Simple("OK")).size, 0)
|
|
395
|
+
}
|
|
396
|
+
|