cj-tauri 1.1.2 → 2.0.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 (101) hide show
  1. package/README.md +64 -18
  2. package/cjpm.toml +52 -52
  3. package/cli/cjpm.toml +10 -1
  4. package/cli/src/doctor.cj +4 -2
  5. package/cli/src/main.cj +11 -3
  6. package/cli/src/manifest.cj +558 -0
  7. package/cli/src/project.cj +38 -22
  8. package/cli/src/scaffold.cj +369 -361
  9. package/cli/src/sync.cj +526 -0
  10. package/cli/src/util.cj +168 -159
  11. package/cli/templates/app/README.md +48 -80
  12. package/cli/templates/app/{capabilities → assets/capabilities}/default.json +7 -7
  13. package/cli/templates/app/{ui → assets/ui}/index.html +118 -118
  14. package/cli/templates/app/cj-tauri.toml +22 -0
  15. package/cli/templates/app/cjpm.toml +34 -17
  16. package/cli/templates/app/src/main.cj +90 -88
  17. package/cli/templates/app-react/README.md +56 -67
  18. package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +7 -7
  19. package/cli/templates/app-react/{ui → assets/ui}/package.json +19 -19
  20. package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +6 -6
  21. package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +24 -24
  22. package/cli/templates/app-react/cj-tauri.toml +23 -0
  23. package/cli/templates/app-react/cjpm.toml +34 -17
  24. package/cli/templates/app-react/src/main.cj +91 -102
  25. package/cli/templates/app-vue/README.md +56 -60
  26. package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +7 -7
  27. package/cli/templates/app-vue/{ui → assets/ui}/package.json +18 -18
  28. package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +4 -4
  29. package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +24 -24
  30. package/cli/templates/app-vue/cj-tauri.toml +23 -0
  31. package/cli/templates/app-vue/cjpm.toml +34 -17
  32. package/cli/templates/app-vue/src/main.cj +91 -102
  33. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +13 -13
  34. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +8 -8
  35. package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +6 -6
  36. package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +79 -79
  37. package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +46 -46
  38. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +26 -26
  39. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +43 -43
  40. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +6 -6
  41. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +9 -9
  42. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +17 -17
  43. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +94 -94
  44. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/index.cj +167 -167
  45. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +139 -139
  46. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +49 -49
  47. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +8 -8
  48. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +16 -16
  49. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +6 -6
  50. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +2 -2
  51. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +6 -6
  52. package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +92 -92
  53. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +5 -5
  54. package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +6 -6
  55. package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +10 -10
  56. package/package.json +4 -2
  57. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  58. package/src/app.cj +49 -20
  59. package/src/app_spec.cj +76 -0
  60. package/src/arkweb_script.cj +142 -154
  61. package/src/host.cj +20 -17
  62. package/src/host_arkweb.cj +778 -740
  63. package/src/host_webkit.cj +22 -0
  64. package/src/host_webview2.cj +22 -0
  65. package/src/launch.cj +62 -0
  66. package/src/platform/platform_app_context.cj +77 -0
  67. package/src/platform/platform_assets.cj +33 -0
  68. package/src/platform/platform_discover.cj +176 -0
  69. package/src/platform/platform_env.cj +29 -0
  70. package/src/platform/platform_feature.cj +31 -0
  71. package/src/platform/platform_page_source.cj +87 -0
  72. package/src/platform/platform_target_info.cj +32 -0
  73. package/src/platform_host_provider.cj +87 -0
  74. package/src/plugin.cj +36 -2
  75. package/src/plugins/clipboard/plugin_clipboard.cj +8 -0
  76. package/src/plugins/menu/plugin_menu.cj +158 -150
  77. package/src/plugins/notification/plugin_notification.cj +8 -0
  78. package/src/plugins/serial/plugin_serial.cj +454 -442
  79. package/src/plugins/serial/serial_bridge.cj +194 -186
  80. package/src/plugins/serial/serial_ohos_stub.cj +61 -0
  81. package/src/plugins/shell/plugin_shell.cj +258 -230
  82. package/src/plugins/sqlite/plugin_sqlite.cj +710 -697
  83. package/src/plugins/sqlite/sqlite_ffi.cj +211 -206
  84. package/src/plugins/sqlite/sqlite_ohos_stub.cj +91 -0
  85. package/src/tests/app_spec_test.cj +127 -0
  86. package/src/tests/fake_host.cj +23 -8
  87. package/src/tests/platform_contract_test.cj +251 -0
  88. package/src/tests/plugin_test.cj +585 -542
  89. package/src/version.cj +1 -1
  90. package/src/host_factory.cj +0 -48
  91. /package/cli/templates/app-react/{ui → assets/ui}/index.html +0 -0
  92. /package/cli/templates/app-react/{ui → assets/ui}/src/App.css +0 -0
  93. /package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +0 -0
  94. /package/cli/templates/app-vue/{ui → assets/ui}/index.html +0 -0
  95. /package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +0 -0
  96. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
  97. /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
  98. /package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +0 -0
  99. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
  100. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
  101. /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
@@ -1,206 +1,211 @@
1
- package cjTauri.plugins.sqlite
2
-
3
- import cjTauri.*
4
-
5
- /**
6
- * SQLite C API 的 FFI 声明与最小包装(`sqlite` 插件的地基)
7
- *
8
- * **为什么直连系统库而不进 C 桥**(docs/设计决策记录.md D-018 第 2 条):现有 `cj_bridge_*` 之所以存在,
9
- * 是因为平台差异集中在 C 桥里;SQLite 相反——三平台同一套 C API、没有平台分支,再包一层只是多一份要
10
- * 维护的转发代码。代价是「链接谁」从「桥提供」变成「消费方各自提供」:Linux 链发行版系统库
11
- * (`-lsqlite3`,本机 3.45.1),Windows 侧未落地(见 D-018 第 8 条)。
12
- *
13
- * **FFI 语法是实测出来的**(三条都进 AGENTS §4):
14
- * 1. 仓颉**没有** `&x` 取地址(写它会报 `expected expression after ','`);C 出参在**调用处**写
15
- * `inout <var>`,自动取址成 `CPointer<T>`——修饰对象只能是 `var`,且指针仅在本次调用期间有效。
16
- * 2. 「可选传 NULL 的 C 字符串」(这里的 `zVfs`)声明成 `CPointer<UInt8>` 并传 `CPointer<UInt8>()`;
17
- * 传空 `CString` 会被 SQLite 当成「名字为空的 VFS」,报的是 `no such vfs`(实测踩过)。
18
- * 3. C 回调声明成 `CFunc<(...) -> 返回类型>`,回调本体是顶层 `@C func`(本仓 `cj_bridge_init` 先例)。
19
- */
20
-
21
- /** SQLite 返回码(sqlite3.h 的子集,只列我们判过的) */
22
- let SQLITE_OK: Int32 = 0
23
- let SQLITE_ROW: Int32 = 100
24
- let SQLITE_DONE: Int32 = 101
25
- /** sqlite3_exec 在授权回调拒绝时给的是 SQLITE_AUTH(列出来便于断言时看明白) */
26
- let SQLITE_AUTH: Int32 = 23
27
-
28
- /** 列类型(sqlite3_column_type):界面要能说清「这格是数字还是文本」,所以逐格判 */
29
- let SQLITE_INTEGER: Int32 = 1
30
- let SQLITE_FLOAT: Int32 = 2
31
- let SQLITE_TEXT: Int32 = 3
32
- let SQLITE_BLOB: Int32 = 4
33
- let SQLITE_NULL: Int32 = 5
34
-
35
- /** 打开标志(sqlite3_open_v2):只读走 READONLY,写路径要显式给 READWRITE */
36
- let SQLITE_OPEN_READONLY: Int32 = 1
37
- let SQLITE_OPEN_READWRITE: Int32 = 2
38
- let SQLITE_OPEN_CREATE: Int32 = 4
39
-
40
- /** 授权动作码(sqlite3_set_authorizer):只列我们要拒的三类 */
41
- let SQLITE_ATTACH: Int32 = 24
42
- let SQLITE_DETACH: Int32 = 25
43
- let SQLITE_FUNCTION: Int32 = 31
44
-
45
- /** 授权回调返回值 */
46
- let SQLITE_DENY: Int32 = 1
47
-
48
- /** busy 等待上限(毫秒):另一个进程正写这个库时等着,而不是当场报 SQLITE_BUSY */
49
- let SQLITE_BUSY_TIMEOUT_MS: Int32 = 3000
50
-
51
- @C
52
- foreign func sqlite3_libversion(): CString
53
-
54
- @C
55
- foreign func sqlite3_open_v2(filename: CString, ppDb: CPointer<CPointer<Unit>>, flags: Int32,
56
- zVfs: CPointer<UInt8>): Int32
57
-
58
- @C
59
- foreign func sqlite3_close(db: CPointer<Unit>): Int32
60
-
61
- @C
62
- foreign func sqlite3_errmsg(db: CPointer<Unit>): CString
63
-
64
- @C
65
- foreign func sqlite3_exec(db: CPointer<Unit>, sql: CString, cb: CPointer<Unit>, arg: CPointer<Unit>,
66
- errmsg: CPointer<Unit>): Int32
67
-
68
- @C
69
- foreign func sqlite3_changes(db: CPointer<Unit>): Int32
70
-
71
- @C
72
- foreign func sqlite3_last_insert_rowid(db: CPointer<Unit>): Int64
73
-
74
- @C
75
- foreign func sqlite3_busy_timeout(db: CPointer<Unit>, ms: Int32): Int32
76
-
77
- @C
78
- foreign func sqlite3_prepare_v2(db: CPointer<Unit>, sql: CString, nByte: Int32,
79
- ppStmt: CPointer<CPointer<Unit>>, pzTail: CPointer<Unit>): Int32
80
-
81
- @C
82
- foreign func sqlite3_step(stmt: CPointer<Unit>): Int32
83
-
84
- @C
85
- foreign func sqlite3_finalize(stmt: CPointer<Unit>): Int32
86
-
87
- @C
88
- foreign func sqlite3_column_count(stmt: CPointer<Unit>): Int32
89
-
90
- @C
91
- foreign func sqlite3_column_name(stmt: CPointer<Unit>, i: Int32): CString
92
-
93
- @C
94
- foreign func sqlite3_column_type(stmt: CPointer<Unit>, i: Int32): Int32
95
-
96
- @C
97
- foreign func sqlite3_column_int64(stmt: CPointer<Unit>, i: Int32): Int64
98
-
99
- @C
100
- foreign func sqlite3_column_double(stmt: CPointer<Unit>, i: Int32): Float64
101
-
102
- @C
103
- foreign func sqlite3_column_text(stmt: CPointer<Unit>, i: Int32): CString
104
-
105
- @C
106
- foreign func sqlite3_column_bytes(stmt: CPointer<Unit>, i: Int32): Int32
107
-
108
- @C
109
- foreign func sqlite3_set_authorizer(
110
- db: CPointer<Unit>,
111
- cb: CFunc<(CPointer<Unit>, Int32, CString, CString, CString, CString) -> Int32>,
112
- userdata: CPointer<Unit>): Int32
113
-
114
- @C
115
- foreign func sqlite3_enable_load_extension(db: CPointer<Unit>, onoff: Int32): Int32
116
-
117
- /**
118
- * 授权回调:拒掉「跑到别的文件去」的动作——`ATTACH` / `DETACH` 换库文件、
119
- * `load_extension()` 加载任意原生库。**必须按语句判**:路径 scope 只管得住本次命令收到的那个路径,
120
- * 管不住 SQL 里写死的另一个路径(D-018 第 4 条)。被拒时留一行 stderr,取证时能对上账。
121
- */
122
- @C
123
- func sqliteAuthorizer(app: CPointer<Unit>, code: Int32, arg1: CString, arg2: CString,
124
- dbName: CString, triggerName: CString): Int32 {
125
- let _ = app
126
- let _ = arg2
127
- let _ = dbName
128
- let _ = triggerName
129
- if (code == SQLITE_ATTACH || code == SQLITE_DETACH) {
130
- eprintln("[cj-tauri] sqlite denied: attach/detach is not allowed (code=${code})")
131
- return SQLITE_DENY
132
- }
133
- if (code == SQLITE_FUNCTION) {
134
- // arg1 是被调用的函数名:只拒能加载原生代码的那个,其余内置函数照常放行
135
- let fnName = sqliteCString(arg1)
136
- if (fnName == "load_extension") {
137
- eprintln("[cj-tauri] sqlite denied: load_extension() is not allowed")
138
- return SQLITE_DENY
139
- }
140
- }
141
- return SQLITE_OK
142
- }
143
-
144
- /** C 字符串 → 仓颉字符串;NULL 给空串(调用方要区分 NULL 与空串时另判归属) */
145
- func sqliteCString(s: CString): String {
146
- if (s.isNull()) {
147
- return ""
148
- }
149
- return s.toString()
150
- }
151
-
152
- /** 取最近一次错误文案;db 为空时给固定文案,避免收尾路径上再炸一次 */
153
- func sqliteLastError(db: CPointer<Unit>): String {
154
- if (db.isNull()) {
155
- return "unknown error (null db handle)"
156
- }
157
- return sqliteCString(unsafe { sqlite3_errmsg(db) })
158
- }
159
-
160
- /** 关掉句柄:对空指针安全(出错路径也要能收尾),关不掉只记一行 stderr 不抛 */
161
- func sqliteCloseQuietly(db: CPointer<Unit>): Unit {
162
- if (db.isNull()) {
163
- return
164
- }
165
- let rc = unsafe { sqlite3_close(db) }
166
- if (rc != SQLITE_OK) {
167
- eprintln("[cj-tauri] sqlite close returned ${rc}")
168
- }
169
- }
170
-
171
- /**
172
- * 打开数据库并装上护栏(每次命令短连接,见 D-018 第 9 条)。
173
- *
174
- * `readonly=true` 走 `SQLITE_OPEN_READONLY`——只读是**内核级**的,不是我们写的检查;
175
- * 不存在的文件 + 只读 = 打开失败(不会顺手造一个空库)。
176
- * 打开失败与护栏装不上都抛 `CommandException`(前端拿到可读文案)。
177
- */
178
- func sqliteOpenGuarded(path: String, readonly: Bool): CPointer<Unit> {
179
- var db: CPointer<Unit> = CPointer<Unit>()
180
- let flags = if (readonly) {
181
- SQLITE_OPEN_READONLY
182
- } else {
183
- SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
184
- }
185
- // inout db 取的是「栈上那个指针变量」的地址:C 侧写入的 handle 直接落到 db 上
186
- let rc = unsafe { sqlite3_open_v2(LibC.mallocCString(path), inout db, flags, CPointer<UInt8>()) }
187
- if (rc != SQLITE_OK) {
188
- let msg = sqliteLastError(db)
189
- sqliteCloseQuietly(db)
190
- throw CommandException("sqlite: cannot open ${path}: ${msg}")
191
- }
192
- // 双保险之一:先关掉扩展加载(authorizer 之外再加一道)
193
- let _ = unsafe { sqlite3_enable_load_extension(db, 0) }
194
- let rcAuthorizer = unsafe { sqlite3_set_authorizer(db, sqliteAuthorizer, CPointer<Unit>()) }
195
- if (rcAuthorizer != SQLITE_OK) {
196
- sqliteCloseQuietly(db)
197
- throw CommandException("sqlite: cannot install authorizer (rc=${rcAuthorizer})")
198
- }
199
- let _ = unsafe { sqlite3_busy_timeout(db, SQLITE_BUSY_TIMEOUT_MS) }
200
- return db
201
- }
202
-
203
- /** 本机链接的 SQLite 版本(`sqlite:info` 回投给页面,对账时也用它) */
204
- func sqliteLibVersion(): String {
205
- return sqliteCString(unsafe { sqlite3_libversion() })
206
- }
1
+ package cjTauri.plugins.sqlite
2
+
3
+ import cjTauri.*
4
+
5
+ /**
6
+ * SQLite C API 的 FFI 声明与最小包装(`sqlite` 插件的地基)
7
+ *
8
+ * **为什么直连系统库而不进 C 桥**(docs/设计决策记录.md D-018 第 2 条):现有 `cj_bridge_*` 之所以存在,
9
+ * 是因为平台差异集中在 C 桥里;SQLite 相反——三平台同一套 C API、没有平台分支,再包一层只是多一份要
10
+ * 维护的转发代码。代价是「链接谁」从「桥提供」变成「消费方各自提供」:Linux 链发行版系统库
11
+ * (`-lsqlite3`,本机 3.45.1),Windows 侧未落地(见 D-018 第 8 条)。
12
+ *
13
+ * **FFI 语法是实测出来的**(三条都进 AGENTS §4):
14
+ * 1. 仓颉**没有** `&x` 取地址(写它会报 `expected expression after ','`);C 出参在**调用处**写
15
+ * `inout <var>`,自动取址成 `CPointer<T>`——修饰对象只能是 `var`,且指针仅在本次调用期间有效。
16
+ * 2. 「可选传 NULL 的 C 字符串」(这里的 `zVfs`)声明成 `CPointer<UInt8>` 并传 `CPointer<UInt8>()`;
17
+ * 传空 `CString` 会被 SQLite 当成「名字为空的 VFS」,报的是 `no such vfs`(实测踩过)。
18
+ * 3. C 回调声明成 `CFunc<(...) -> 返回类型>`,回调本体是顶层 `@C func`(本仓 `cj_bridge_init` 先例)。
19
+ */
20
+
21
+ /** SQLite 返回码(sqlite3.h 的子集,只列我们判过的) */
22
+ let SQLITE_OK: Int32 = 0
23
+ let SQLITE_ROW: Int32 = 100
24
+ let SQLITE_DONE: Int32 = 101
25
+ /** sqlite3_exec 在授权回调拒绝时给的是 SQLITE_AUTH(列出来便于断言时看明白) */
26
+ let SQLITE_AUTH: Int32 = 23
27
+
28
+ /** 列类型(sqlite3_column_type):界面要能说清「这格是数字还是文本」,所以逐格判 */
29
+ let SQLITE_INTEGER: Int32 = 1
30
+ let SQLITE_FLOAT: Int32 = 2
31
+ let SQLITE_TEXT: Int32 = 3
32
+ let SQLITE_BLOB: Int32 = 4
33
+ let SQLITE_NULL: Int32 = 5
34
+
35
+ /** 打开标志(sqlite3_open_v2):只读走 READONLY,写路径要显式给 READWRITE */
36
+ let SQLITE_OPEN_READONLY: Int32 = 1
37
+ let SQLITE_OPEN_READWRITE: Int32 = 2
38
+ let SQLITE_OPEN_CREATE: Int32 = 4
39
+
40
+ /** 授权动作码(sqlite3_set_authorizer):只列我们要拒的三类 */
41
+ let SQLITE_ATTACH: Int32 = 24
42
+ let SQLITE_DETACH: Int32 = 25
43
+ let SQLITE_FUNCTION: Int32 = 31
44
+
45
+ /** 授权回调返回值 */
46
+ let SQLITE_DENY: Int32 = 1
47
+
48
+ /** busy 等待上限(毫秒):另一个进程正写这个库时等着,而不是当场报 SQLITE_BUSY */
49
+ let SQLITE_BUSY_TIMEOUT_MS: Int32 = 3000
50
+
51
+ @C
52
+ foreign func sqlite3_libversion(): CString
53
+
54
+ @C
55
+ foreign func sqlite3_open_v2(filename: CString, ppDb: CPointer<CPointer<Unit>>, flags: Int32,
56
+ zVfs: CPointer<UInt8>): Int32
57
+
58
+ @C
59
+ foreign func sqlite3_close(db: CPointer<Unit>): Int32
60
+
61
+ @C
62
+ foreign func sqlite3_errmsg(db: CPointer<Unit>): CString
63
+
64
+ @C
65
+ foreign func sqlite3_exec(db: CPointer<Unit>, sql: CString, cb: CPointer<Unit>, arg: CPointer<Unit>,
66
+ errmsg: CPointer<Unit>): Int32
67
+
68
+ @C
69
+ foreign func sqlite3_changes(db: CPointer<Unit>): Int32
70
+
71
+ @C
72
+ foreign func sqlite3_last_insert_rowid(db: CPointer<Unit>): Int64
73
+
74
+ @C
75
+ foreign func sqlite3_busy_timeout(db: CPointer<Unit>, ms: Int32): Int32
76
+
77
+ @C
78
+ foreign func sqlite3_prepare_v2(db: CPointer<Unit>, sql: CString, nByte: Int32,
79
+ ppStmt: CPointer<CPointer<Unit>>, pzTail: CPointer<Unit>): Int32
80
+
81
+ @C
82
+ foreign func sqlite3_step(stmt: CPointer<Unit>): Int32
83
+
84
+ @C
85
+ foreign func sqlite3_finalize(stmt: CPointer<Unit>): Int32
86
+
87
+ @C
88
+ foreign func sqlite3_column_count(stmt: CPointer<Unit>): Int32
89
+
90
+ @C
91
+ foreign func sqlite3_column_name(stmt: CPointer<Unit>, i: Int32): CString
92
+
93
+ @C
94
+ foreign func sqlite3_column_type(stmt: CPointer<Unit>, i: Int32): Int32
95
+
96
+ @C
97
+ foreign func sqlite3_column_int64(stmt: CPointer<Unit>, i: Int32): Int64
98
+
99
+ @C
100
+ foreign func sqlite3_column_double(stmt: CPointer<Unit>, i: Int32): Float64
101
+
102
+ @C
103
+ foreign func sqlite3_column_text(stmt: CPointer<Unit>, i: Int32): CString
104
+
105
+ @C
106
+ foreign func sqlite3_column_bytes(stmt: CPointer<Unit>, i: Int32): Int32
107
+
108
+ @C
109
+ foreign func sqlite3_set_authorizer(
110
+ db: CPointer<Unit>,
111
+ cb: CFunc<(CPointer<Unit>, Int32, CString, CString, CString, CString) -> Int32>,
112
+ userdata: CPointer<Unit>): Int32
113
+
114
+ @C
115
+ foreign func sqlite3_enable_load_extension(db: CPointer<Unit>, onoff: Int32): Int32
116
+
117
+ /**
118
+ * 授权回调:拒掉「跑到别的文件去」的动作——`ATTACH` / `DETACH` 换库文件、
119
+ * `load_extension()` 加载任意原生库。**必须按语句判**:路径 scope 只管得住本次命令收到的那个路径,
120
+ * 管不住 SQL 里写死的另一个路径(D-018 第 4 条)。被拒时留一行 stderr,取证时能对上账。
121
+ */
122
+ @When[env != "ohos"]
123
+ @C
124
+ func sqliteAuthorizer(app: CPointer<Unit>, code: Int32, arg1: CString, arg2: CString,
125
+ dbName: CString, triggerName: CString): Int32 {
126
+ let _ = app
127
+ let _ = arg2
128
+ let _ = dbName
129
+ let _ = triggerName
130
+ if (code == SQLITE_ATTACH || code == SQLITE_DETACH) {
131
+ eprintln("[cj-tauri] sqlite denied: attach/detach is not allowed (code=${code})")
132
+ return SQLITE_DENY
133
+ }
134
+ if (code == SQLITE_FUNCTION) {
135
+ // arg1 是被调用的函数名:只拒能加载原生代码的那个,其余内置函数照常放行
136
+ let fnName = sqliteCString(arg1)
137
+ if (fnName == "load_extension") {
138
+ eprintln("[cj-tauri] sqlite denied: load_extension() is not allowed")
139
+ return SQLITE_DENY
140
+ }
141
+ }
142
+ return SQLITE_OK
143
+ }
144
+
145
+ /** C 字符串 → 仓颉字符串;NULL 给空串(调用方要区分 NULL 与空串时另判归属) */
146
+ func sqliteCString(s: CString): String {
147
+ if (s.isNull()) {
148
+ return ""
149
+ }
150
+ return s.toString()
151
+ }
152
+
153
+ /** 取最近一次错误文案;db 为空时给固定文案,避免收尾路径上再炸一次 */
154
+ @When[env != "ohos"]
155
+ func sqliteLastError(db: CPointer<Unit>): String {
156
+ if (db.isNull()) {
157
+ return "unknown error (null db handle)"
158
+ }
159
+ return sqliteCString(unsafe { sqlite3_errmsg(db) })
160
+ }
161
+
162
+ /** 关掉句柄:对空指针安全(出错路径也要能收尾),关不掉只记一行 stderr 不抛 */
163
+ @When[env != "ohos"]
164
+ func sqliteCloseQuietly(db: CPointer<Unit>): Unit {
165
+ if (db.isNull()) {
166
+ return
167
+ }
168
+ let rc = unsafe { sqlite3_close(db) }
169
+ if (rc != SQLITE_OK) {
170
+ eprintln("[cj-tauri] sqlite close returned ${rc}")
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 打开数据库并装上护栏(每次命令短连接,见 D-018 第 9 条)。
176
+ *
177
+ * `readonly=true` 走 `SQLITE_OPEN_READONLY`——只读是**内核级**的,不是我们写的检查;
178
+ * 不存在的文件 + 只读 = 打开失败(不会顺手造一个空库)。
179
+ * 打开失败与护栏装不上都抛 `CommandException`(前端拿到可读文案)。
180
+ */
181
+ @When[env != "ohos"]
182
+ func sqliteOpenGuarded(path: String, readonly: Bool): CPointer<Unit> {
183
+ var db: CPointer<Unit> = CPointer<Unit>()
184
+ let flags = if (readonly) {
185
+ SQLITE_OPEN_READONLY
186
+ } else {
187
+ SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
188
+ }
189
+ // inout db 取的是「栈上那个指针变量」的地址:C 侧写入的 handle 直接落到 db 上
190
+ let rc = unsafe { sqlite3_open_v2(LibC.mallocCString(path), inout db, flags, CPointer<UInt8>()) }
191
+ if (rc != SQLITE_OK) {
192
+ let msg = sqliteLastError(db)
193
+ sqliteCloseQuietly(db)
194
+ throw CommandException("sqlite: cannot open ${path}: ${msg}")
195
+ }
196
+ // 双保险之一:先关掉扩展加载(authorizer 之外再加一道)
197
+ let _ = unsafe { sqlite3_enable_load_extension(db, 0) }
198
+ let rcAuthorizer = unsafe { sqlite3_set_authorizer(db, sqliteAuthorizer, CPointer<Unit>()) }
199
+ if (rcAuthorizer != SQLITE_OK) {
200
+ sqliteCloseQuietly(db)
201
+ throw CommandException("sqlite: cannot install authorizer (rc=${rcAuthorizer})")
202
+ }
203
+ let _ = unsafe { sqlite3_busy_timeout(db, SQLITE_BUSY_TIMEOUT_MS) }
204
+ return db
205
+ }
206
+
207
+ /** 本机链接的 SQLite 版本(`sqlite:info` 回投给页面,对账时也用它) */
208
+ @When[env != "ohos"]
209
+ func sqliteLibVersion(): String {
210
+ return sqliteCString(unsafe { sqlite3_libversion() })
211
+ }
@@ -0,0 +1,91 @@
1
+ package cjTauri.plugins.sqlite
2
+
3
+ /**
4
+ * ohos 侧 sqlite 桩(RFC §4.6.4 P-1):FFI 直连系统 sqlite3 在鸿蒙 hap 里没有链接对象,
5
+ * 桌面实现整体门禁(plugin_sqlite.cj / sqlite_ffi.cj 的 `@When[env != "ohos"]`),
6
+ * 本文件提供同名的桩类占住装配位——业务 build() 三端共用、import 全部所用插件,
7
+ * 不能靠编译期排除子包(与「业务零平台分支」冲突,RFC §4.6.4 有完整论证)。
8
+ *
9
+ * 桩语义:命令表 / 权限集 / 前端 shim 与桌面版完全一致(页面 API 面不变),
10
+ * 但每条命令运行期抛「端不支持」——P-2 的如实失败,与 describe().supportedHosts
11
+ * (webkitgtk)严格一致。真实现批次 4 走 NDK FFI 接通(§4.6.5 四通道决策)后,
12
+ * 删掉本文件、在桌面声明上补 ohos 实现段。
13
+ */
14
+
15
+ @When[env == "ohos"]
16
+ import std.collection.*
17
+
18
+ @When[env == "ohos"]
19
+ import stdx.encoding.json.*
20
+
21
+ @When[env == "ohos"]
22
+ import cjTauri.*
23
+
24
+ @When[env == "ohos"]
25
+ public class SqlitePlugin <: Plugin {
26
+ public init() {}
27
+
28
+ /** 自描述与桌面版一致(P-2:桩行为与 supportedHosts 严格对齐) */
29
+ public func describe(): PluginManifest {
30
+ let hosts = ArrayList<String>()
31
+ hosts.add("webkitgtk")
32
+ return PluginManifest(hosts, ArrayList<String>())
33
+ }
34
+
35
+ public func name(): String {
36
+ return "sqlite"
37
+ }
38
+
39
+ /** 与桌面版一致:path 参数吃 scope 校验(桩不碰路径,语义保持一致免得两端行为分叉) */
40
+ public func scopeAware(): Bool {
41
+ return true
42
+ }
43
+
44
+ /** 命令表与桌面版同形(常量共用 plugin_sqlite.cj 的门禁外 let),每条指向桩处理器 */
45
+ public func commands(): HashMap<String, CommandHandler> {
46
+ let m = HashMap<String, CommandHandler>()
47
+ for (short in SQLITE_PLUGIN_COMMANDS) {
48
+ m.add(short, SqliteOhosStubHandler())
49
+ }
50
+ return m
51
+ }
52
+
53
+ public func permissions(): HashMap<String, ArrayList<String>> {
54
+ let m = HashMap<String, ArrayList<String>>()
55
+ let ro = ArrayList<String>()
56
+ for (short in SQLITE_READONLY_COMMANDS) {
57
+ ro.add(short)
58
+ }
59
+ m.add("readonly", ro)
60
+ let all = ArrayList<String>()
61
+ for (short in SQLITE_PLUGIN_COMMANDS) {
62
+ all.add(short)
63
+ }
64
+ m.add("default", all)
65
+ return m
66
+ }
67
+
68
+ /** 前端 shim 与桌面版逐字一致:页面 __CJ_TAURI__.sqlite 命名空间照常存在,调用得到可读错误 */
69
+ public func jsShim(): String {
70
+ return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
71
+ " t.sqlite = {" +
72
+ " info: function (args) { return window.__CJ_TAURI__.invoke('sqlite:info', args); }," +
73
+ " tables: function (args) { return window.__CJ_TAURI__.invoke('sqlite:tables', args); }," +
74
+ " schema: function (args) { return window.__CJ_TAURI__.invoke('sqlite:schema', args); }," +
75
+ " page: function (args) { return window.__CJ_TAURI__.invoke('sqlite:page', args); }," +
76
+ " query: function (args) { return window.__CJ_TAURI__.invoke('sqlite:query', args); }," +
77
+ " exec: function (args) { return window.__CJ_TAURI__.invoke('sqlite:exec', args); }," +
78
+ " env: function (args) { return window.__CJ_TAURI__.invoke('sqlite:env', args); }" +
79
+ " }; })();\n"
80
+ }
81
+ }
82
+
83
+ /** 桩处理器:不分命令、一律如实报错(可照做:换端或等批次 4) */
84
+ @When[env == "ohos"]
85
+ class SqliteOhosStubHandler <: CommandHandler {
86
+ public init() {}
87
+
88
+ public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
89
+ throw CommandException("sqlite: ${cmd} 在 ohos 端暂不支持(P-1 桩,批次 4 走 NDK FFI 接通,见 RFC §4.6.4)")
90
+ }
91
+ }
@@ -0,0 +1,127 @@
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import cjTauri.platform.*
5
+ import cjTauri.plugins.store.*
6
+ import std.collection.*
7
+ import std.time.*
8
+ import std.unittest.*
9
+ import std.unittest.testmacro.*
10
+
11
+ // ---- 批次 3-PR1:AppSpec 构造器链与 assemble 装配 ----
12
+
13
+ @Test
14
+ func appSpecBuilderChain(): Unit {
15
+ let spec = AppSpec()
16
+ .page(PageSource.Inline("<p/>"))
17
+ .window(WindowConfig("t", 800, 600))
18
+ .capabilities("assets/capabilities")
19
+ .capabilityJson("{\"identifier\":\"default\"}")
20
+ @Expect(spec.appId, "")
21
+ @Expect(spec.capabilitiesDir, "assets/capabilities")
22
+ @Expect(spec.capabilityJsons.size, 1)
23
+ @Expect(spec.windowConfig.title, "t")
24
+ @Expect(spec.windowConfig.width, 800)
25
+ @Expect(spec.assetResolver.isSome(), false)
26
+ @Expect(spec.wireFn.isSome(), false)
27
+ @Expect(spec.plugins.size, 0)
28
+ match (spec.pageSource) {
29
+ case Inline(html) => @Expect(html, "<p/>")
30
+ case _ => @Expect(false, true)
31
+ }
32
+ }
33
+
34
+ /** wire 闭包执行标记(仓颉闭包捕获 var 的语义不冒风险,用对象装状态) */
35
+ class WireBox {
36
+ public var on: Bool = false
37
+ }
38
+
39
+ /** 高优先级假宿主:TauriApp 字段初始化就调 resolveHost,无显示环境必须用注册表桩 */
40
+ class AppSpecProbeProvider <: HostProvider {
41
+ public var last: FakeHost = FakeHost("seed")
42
+
43
+ public func priority(target: TargetInfo): Int64 {
44
+ return 100
45
+ }
46
+
47
+ public func create(label: String, target: TargetInfo): WebViewHost {
48
+ this.last = FakeHost(label)
49
+ return this.last
50
+ }
51
+ }
52
+
53
+ @Test
54
+ func appSpecAssembleWiresApp(): Unit {
55
+ let prov = AppSpecProbeProvider()
56
+ registerHost(prov)
57
+ let box = WireBox()
58
+ let spec = AppSpec()
59
+ .page(PageSource.Inline("<p/>"))
60
+ .window(WindowConfig("spec窗", 640, 480))
61
+ .plugin(StorePlugin())
62
+ .wire({ app =>
63
+ box.on = true
64
+ })
65
+ let app = assemble(spec)
66
+ @Expect(box.on, true)
67
+ @Expect(app.hostOf("main").isSome(), true)
68
+ @Expect(prov.last.label(), "main")
69
+ }
70
+
71
+ // ---- 批次 3-PR3:load(source) 收敛——装配层把 PageSource 原样交给宿主,resolver 随装配进宿主 ----
72
+
73
+ @Test
74
+ func fakeHostLoadDispatchesPageSource(): Unit {
75
+ let h = FakeHost("main")
76
+ h.load(PageSource.Inline("<p>hi</p>"))
77
+ @Expect(h.shownHtml, "<p>hi</p>")
78
+ @Expect(h.startedUrl, "")
79
+ h.load(PageSource.Url("https://example.com"))
80
+ @Expect(h.startedUrl, "https://example.com")
81
+ h.load(PageSource.DevServer("http://127.0.0.1:5173"))
82
+ @Expect(h.startedUrl, "http://127.0.0.1:5173")
83
+ h.load(PageSource.Asset("ui/index.html"))
84
+ @Expect(h.loadedSource, "asset:ui/index.html")
85
+ }
86
+
87
+ /** run 路径探针:优先级压过既有注册表里的桩,保证本用例拿到的是自己造的宿主 */
88
+ class RunProbeProvider <: HostProvider {
89
+ public var last: FakeHost = FakeHost("seed")
90
+
91
+ public func priority(target: TargetInfo): Int64 {
92
+ return 200
93
+ }
94
+
95
+ public func create(label: String, target: TargetInfo): WebViewHost {
96
+ this.last = FakeHost(label)
97
+ return this.last
98
+ }
99
+ }
100
+
101
+ /**
102
+ * run() 路径:resolver 注入与页面来源交接发生在同一次装配(runWith)里。
103
+ * 宿主用 FakeHost 桩(不建窗口);run() 放后台线程,观测到交接后置 quitNow 让它返回。
104
+ */
105
+ @Test
106
+ func runHandsResolverAndSourceToHost(): Unit {
107
+ let prov = RunProbeProvider()
108
+ let provIdx = registerHost(prov)
109
+ let spec = AppSpec()
110
+ .page(PageSource.Asset("ui/index.html"))
111
+ .window(WindowConfig("t", 640, 480))
112
+ .assets(StubResolver(HashMap()))
113
+ spawn {
114
+ let app = assemble(spec)
115
+ app.run(spec.pageSource)
116
+ }
117
+ var polls = 0
118
+ while (polls < 1000 && prov.last.loadedSource != "asset:ui/index.html") {
119
+ sleep(Duration.millisecond * 2)
120
+ polls += 1
121
+ }
122
+ @Expect(prov.last.loadedSource, "asset:ui/index.html")
123
+ @Expect(prov.last.resolverInjected.isSome(), true)
124
+ prov.last.quitNow = true
125
+ // 探针宿主清出全局注册表,免得压过后续用例自己的桩
126
+ unregisterHost(provIdx)
127
+ }