cj-tauri 2.0.0 → 2.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 +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
|
@@ -1,710 +1,705 @@
|
|
|
1
|
-
package cjTauri.plugins.sqlite
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
|
|
5
|
-
import std.collection.*
|
|
6
|
-
import std.env.*
|
|
7
|
-
import std.fs.*
|
|
8
|
-
import stdx.encoding.json.*
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 官方插件 sqlite:SQLite 浏览与查询(只看本地库文件,FFI 直连系统 sqlite3)
|
|
12
|
-
*
|
|
13
|
-
* 设计取舍见 `docs/设计决策记录.md` 的 **D-018**(以插件交付 + FFI 直连第三方库 + 任意 SQL 的边界)。
|
|
14
|
-
* FFI 声明与护栏在 `sqlite_ffi.cj`(同目录:判据仍是 AGENTS §2 的「谁调用谁包内」)。
|
|
15
|
-
*
|
|
16
|
-
* 命令:`sqlite:info` / `tables` / `schema` / `page` / `query` / `exec` / `env`
|
|
17
|
-
* 权限:**不自动放行**。浏览与改库是两件事,所以两个命名集:
|
|
18
|
-
* `"permissions": ["sqlite:readonly"]` —— info / tables / schema / page / query / env
|
|
19
|
-
* `"permissions": ["sqlite:default"]` —— 再多一条 exec(写路径)
|
|
20
|
-
* 路径:`scopeAware()` 为真——页面给的 `path` 一律经 `IpcContext.resolveScopedPath("sqlite", …)` 校验
|
|
21
|
-
* (命令放行 ≠ 路径放行);清单没为 sqlite 写 scopes 时框架按旧行为全量放行并提示。
|
|
22
|
-
* 前端:`window.__CJ_TAURI__.sqlite.tables({ path: "sample.db" })`
|
|
23
|
-
*
|
|
24
|
-
* **边界如实(D-018)**:
|
|
25
|
-
* - 任意 SQL 只对「已授权的那一个库文件」生效;`ATTACH` / `DETACH` / `load_extension()` 按语句拒
|
|
26
|
-
* (authorizer + 关掉扩展加载),拒了在桥 stderr 留一行 `sqlite denied: …`,不静默。
|
|
27
|
-
* - 只读命令用 `SQLITE_OPEN_READONLY` 打开(内核级只读,不是我们写的检查);`exec` 才给写入标志。
|
|
28
|
-
* - 每命令短连接(开→干活→关),不做连接池 / 会话保持。
|
|
29
|
-
* - **Windows 侧未验证**:本插件链系统 sqlite3,mingw 侧要自带 DLL,本轮不落地(D-018 第 8 条)。
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/** 插件全部命令短名(`permissions()` 的两个集都从这里派生,别再抄第二份) */
|
|
33
|
-
public let SQLITE_PLUGIN_COMMANDS: Array<String> = ["info", "tables", "schema", "page", "query",
|
|
34
|
-
"exec", "env"]
|
|
35
|
-
|
|
36
|
-
/** 只观不控的那些(`readonly` 集):能跑 SELECT,但拿不到 exec */
|
|
37
|
-
let SQLITE_READONLY_COMMANDS: Array<String> = ["info", "tables", "schema", "page", "query", "env"]
|
|
38
|
-
|
|
39
|
-
/** 单页 / 单次查询最多回投的行数:页面要的是「看一眼」,不是导库 */
|
|
40
|
-
let SQLITE_PAGE_MAX: Int64 = 500
|
|
41
|
-
|
|
42
|
-
/** 单元格文本上限(字节):超了只给前段并标 truncated——截断要说,同 ② 的口径 */
|
|
43
|
-
let SQLITE_TEXT_CELL_LIMIT: Int64 = 65536
|
|
44
|
-
|
|
45
|
-
/** `tables` 一次最多列多少个对象(正常库远小于这个数,超了如实置 truncated) */
|
|
46
|
-
let SQLITE_TABLE_LIMIT: Int64 = 1000
|
|
47
|
-
|
|
48
|
-
/** 自检 / 夹具库路径交给页面的环境变量名(页面读不到进程环境,只能由宿主转交) */
|
|
49
|
-
let SQLITE_ENV_SELFCHECK: String = "CJ_SQLITE_SELFCHECK"
|
|
50
|
-
let SQLITE_ENV_FIXTURE: String = "CJ_SQLITE_FIXTURE"
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* `sqlite` 插件:只声明「我提供什么」,放行由应用的 capabilities 决定(RFC §5.3)
|
|
54
|
-
*/
|
|
55
|
-
// 桌面实现;ohos 侧的同名桩在 sqlite_ohos_stub.cj(RFC §4.6.4 P-1:编得过、调用即可照做地报错)
|
|
56
|
-
|
|
57
|
-
public
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
hosts
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
m
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* `
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
cell
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
cell
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
cell
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
cell.put("
|
|
216
|
-
cell
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
throw CommandException("sqlite:
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
case "sqlite:
|
|
313
|
-
case "sqlite:
|
|
314
|
-
case
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
private func
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
case
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
case
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
case
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
let
|
|
420
|
-
|
|
421
|
-
let
|
|
422
|
-
let
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
o.put("
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
o.put("
|
|
462
|
-
o.put("
|
|
463
|
-
o.put("
|
|
464
|
-
o.put("
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
let
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
let
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
let
|
|
580
|
-
let
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
let
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
o.put("
|
|
608
|
-
o.put("
|
|
609
|
-
o.put("
|
|
610
|
-
o
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
o.put("
|
|
642
|
-
o
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
o
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
return fallback
|
|
709
|
-
}
|
|
710
|
-
}
|
|
1
|
+
package cjTauri.plugins.sqlite
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.env.*
|
|
7
|
+
import std.fs.*
|
|
8
|
+
import stdx.encoding.json.*
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 官方插件 sqlite:SQLite 浏览与查询(只看本地库文件,FFI 直连系统 sqlite3)
|
|
12
|
+
*
|
|
13
|
+
* 设计取舍见 `docs/设计决策记录.md` 的 **D-018**(以插件交付 + FFI 直连第三方库 + 任意 SQL 的边界)。
|
|
14
|
+
* FFI 声明与护栏在 `sqlite_ffi.cj`(同目录:判据仍是 AGENTS §2 的「谁调用谁包内」)。
|
|
15
|
+
*
|
|
16
|
+
* 命令:`sqlite:info` / `tables` / `schema` / `page` / `query` / `exec` / `env`
|
|
17
|
+
* 权限:**不自动放行**。浏览与改库是两件事,所以两个命名集:
|
|
18
|
+
* `"permissions": ["sqlite:readonly"]` —— info / tables / schema / page / query / env
|
|
19
|
+
* `"permissions": ["sqlite:default"]` —— 再多一条 exec(写路径)
|
|
20
|
+
* 路径:`scopeAware()` 为真——页面给的 `path` 一律经 `IpcContext.resolveScopedPath("sqlite", …)` 校验
|
|
21
|
+
* (命令放行 ≠ 路径放行);清单没为 sqlite 写 scopes 时框架按旧行为全量放行并提示。
|
|
22
|
+
* 前端:`window.__CJ_TAURI__.sqlite.tables({ path: "sample.db" })`
|
|
23
|
+
*
|
|
24
|
+
* **边界如实(D-018)**:
|
|
25
|
+
* - 任意 SQL 只对「已授权的那一个库文件」生效;`ATTACH` / `DETACH` / `load_extension()` 按语句拒
|
|
26
|
+
* (authorizer + 关掉扩展加载),拒了在桥 stderr 留一行 `sqlite denied: …`,不静默。
|
|
27
|
+
* - 只读命令用 `SQLITE_OPEN_READONLY` 打开(内核级只读,不是我们写的检查);`exec` 才给写入标志。
|
|
28
|
+
* - 每命令短连接(开→干活→关),不做连接池 / 会话保持。
|
|
29
|
+
* - **Windows 侧未验证**:本插件链系统 sqlite3,mingw 侧要自带 DLL,本轮不落地(D-018 第 8 条)。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** 插件全部命令短名(`permissions()` 的两个集都从这里派生,别再抄第二份) */
|
|
33
|
+
public let SQLITE_PLUGIN_COMMANDS: Array<String> = ["info", "tables", "schema", "page", "query",
|
|
34
|
+
"exec", "env"]
|
|
35
|
+
|
|
36
|
+
/** 只观不控的那些(`readonly` 集):能跑 SELECT,但拿不到 exec */
|
|
37
|
+
let SQLITE_READONLY_COMMANDS: Array<String> = ["info", "tables", "schema", "page", "query", "env"]
|
|
38
|
+
|
|
39
|
+
/** 单页 / 单次查询最多回投的行数:页面要的是「看一眼」,不是导库 */
|
|
40
|
+
let SQLITE_PAGE_MAX: Int64 = 500
|
|
41
|
+
|
|
42
|
+
/** 单元格文本上限(字节):超了只给前段并标 truncated——截断要说,同 ② 的口径 */
|
|
43
|
+
let SQLITE_TEXT_CELL_LIMIT: Int64 = 65536
|
|
44
|
+
|
|
45
|
+
/** `tables` 一次最多列多少个对象(正常库远小于这个数,超了如实置 truncated) */
|
|
46
|
+
let SQLITE_TABLE_LIMIT: Int64 = 1000
|
|
47
|
+
|
|
48
|
+
/** 自检 / 夹具库路径交给页面的环境变量名(页面读不到进程环境,只能由宿主转交) */
|
|
49
|
+
let SQLITE_ENV_SELFCHECK: String = "CJ_SQLITE_SELFCHECK"
|
|
50
|
+
let SQLITE_ENV_FIXTURE: String = "CJ_SQLITE_FIXTURE"
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `sqlite` 插件:只声明「我提供什么」,放行由应用的 capabilities 决定(RFC §5.3)
|
|
54
|
+
*/
|
|
55
|
+
// 桌面实现;ohos 侧的同名桩在 sqlite_ohos_stub.cj(RFC §4.6.4 P-1:编得过、调用即可照做地报错)
|
|
56
|
+
public class SqlitePlugin <: Plugin {
|
|
57
|
+
public init() {}
|
|
58
|
+
|
|
59
|
+
/** 自描述:FFI 直连系统 sqlite3,仅 Linux 桌面验证过(Windows 未落地、ohos 未验证) */
|
|
60
|
+
public func describe(): PluginManifest {
|
|
61
|
+
let hosts = ArrayList<String>()
|
|
62
|
+
hosts.add("webkitgtk")
|
|
63
|
+
return PluginManifest(hosts, ArrayList<String>())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public func name(): String {
|
|
67
|
+
return "sqlite"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 本插件的 path 参数吃 scope 校验(D-018 第 3 条):库文件是页面可控路径 */
|
|
71
|
+
public func scopeAware(): Bool {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
76
|
+
let m = HashMap<String, CommandHandler>()
|
|
77
|
+
for (short in SQLITE_PLUGIN_COMMANDS) {
|
|
78
|
+
m.add(short, SqliteCommands())
|
|
79
|
+
}
|
|
80
|
+
return m
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 命名权限集:`readonly` 只含只读命令,`default` 是全集。
|
|
85
|
+
* 只浏览库的应用引用 readonly 即可,**写能力不会被顺带打开**;要写必须显式引用 default。
|
|
86
|
+
* 集内写短名,框架自动补成 `sqlite:<短名>`。
|
|
87
|
+
*/
|
|
88
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
89
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
90
|
+
let ro = ArrayList<String>()
|
|
91
|
+
for (short in SQLITE_READONLY_COMMANDS) {
|
|
92
|
+
ro.add(short)
|
|
93
|
+
}
|
|
94
|
+
m.add("readonly", ro)
|
|
95
|
+
let all = ArrayList<String>()
|
|
96
|
+
for (short in SQLITE_PLUGIN_COMMANDS) {
|
|
97
|
+
all.add(short)
|
|
98
|
+
}
|
|
99
|
+
m.add("default", all)
|
|
100
|
+
return m
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 前端 shim:只挂 `__CJ_TAURI__.sqlite` 一个命名空间,不碰 invoke / listen / emit */
|
|
104
|
+
public func jsShim(): String {
|
|
105
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
106
|
+
" t.sqlite = {" +
|
|
107
|
+
" info: function (args) { return window.__CJ_TAURI__.invoke('sqlite:info', args); }," +
|
|
108
|
+
" tables: function (args) { return window.__CJ_TAURI__.invoke('sqlite:tables', args); }," +
|
|
109
|
+
" schema: function (args) { return window.__CJ_TAURI__.invoke('sqlite:schema', args); }," +
|
|
110
|
+
" page: function (args) { return window.__CJ_TAURI__.invoke('sqlite:page', args); }," +
|
|
111
|
+
" query: function (args) { return window.__CJ_TAURI__.invoke('sqlite:query', args); }," +
|
|
112
|
+
" exec: function (args) { return window.__CJ_TAURI__.invoke('sqlite:exec', args); }," +
|
|
113
|
+
" env: function (args) { return window.__CJ_TAURI__.invoke('sqlite:env', args); }" +
|
|
114
|
+
" }; })();\n"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 一次查询的结果。
|
|
120
|
+
*
|
|
121
|
+
* `rows` 是**按类型逐格**的 JSON(D-018 第 7 条:界面要能说清「这格是数字还是文本」),
|
|
122
|
+
* `plain` 是同一批数据的纯文本渲染——插件内部(列名、表名、计数)用它,省得到处拆单元格对象。
|
|
123
|
+
*/
|
|
124
|
+
class SqliteRows {
|
|
125
|
+
let columns = ArrayList<String>()
|
|
126
|
+
let rows = ArrayList<ArrayList<JsonValue>>()
|
|
127
|
+
let plain = ArrayList<ArrayList<String>>()
|
|
128
|
+
var truncated = false
|
|
129
|
+
|
|
130
|
+
func columnIndex(name: String): Int64 {
|
|
131
|
+
for (i in 0..this.columns.size) {
|
|
132
|
+
if (this.columns[i] == name) {
|
|
133
|
+
return i
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return -1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 取某格文本;越界给空串(调用方按位置取值时自己保证下标) */
|
|
140
|
+
func textAt(row: Int64, col: Int64): String {
|
|
141
|
+
if (row < 0 || row >= this.plain.size) {
|
|
142
|
+
return ""
|
|
143
|
+
}
|
|
144
|
+
let r = this.plain[row]
|
|
145
|
+
if (col < 0 || col >= r.size) {
|
|
146
|
+
return ""
|
|
147
|
+
}
|
|
148
|
+
return r[col]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 表名 / 列名做标识符引用:内部的双引号翻倍(SQL 标准的引用方式),不做字符串插值 */
|
|
153
|
+
func sqliteQuoteIdent(name: String): String {
|
|
154
|
+
return "\"" + name.replace("\"", "\"\"") + "\""
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 按 UTF-8 字符边界截到 limit 字节以内。
|
|
159
|
+
* AGENTS §4 的坑:仓颉迭代 String 拿到的是**字节**,直接按字节切会把汉字的 3 字节切一半
|
|
160
|
+
* (切出非法 UTF-8,页面显示成乱码)。所以回退到「不是续字节」的位置再切。
|
|
161
|
+
*/
|
|
162
|
+
func sqliteClampUtf8(s: String, limit: Int64): String {
|
|
163
|
+
let bytes = s.toArray()
|
|
164
|
+
if (Int64(bytes.size) <= limit) {
|
|
165
|
+
return s
|
|
166
|
+
}
|
|
167
|
+
var end = limit
|
|
168
|
+
while (end > 0 && (bytes[end] & 0xC0u8) == 0x80u8) {
|
|
169
|
+
end = end - 1
|
|
170
|
+
}
|
|
171
|
+
return String.fromUtf8(bytes[0..end])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 取一格的「按类型」JSON 与纯文本渲染。
|
|
176
|
+
* NULL 不给 value(前端显示成 NULL 而不是空串);BLOB 只给字节数(同 ②「二进制不往 IPC 里塞」)。
|
|
177
|
+
*/
|
|
178
|
+
func sqliteReadCell(stmt: CPointer<Unit>, i: Int32): (JsonValue, String) {
|
|
179
|
+
let cell = JsonObject()
|
|
180
|
+
let kind = unsafe { sqlite3_column_type(stmt, i) }
|
|
181
|
+
if (kind == SQLITE_NULL) {
|
|
182
|
+
cell.put("type", JsonString("null"))
|
|
183
|
+
return (cell, "")
|
|
184
|
+
}
|
|
185
|
+
if (kind == SQLITE_INTEGER) {
|
|
186
|
+
let v = unsafe { sqlite3_column_int64(stmt, i) }
|
|
187
|
+
cell.put("type", JsonString("integer"))
|
|
188
|
+
cell.put("value", JsonInt(v))
|
|
189
|
+
return (cell, "${v}")
|
|
190
|
+
}
|
|
191
|
+
if (kind == SQLITE_FLOAT) {
|
|
192
|
+
let v = unsafe { sqlite3_column_double(stmt, i) }
|
|
193
|
+
cell.put("type", JsonString("float"))
|
|
194
|
+
cell.put("value", JsonFloat(v))
|
|
195
|
+
return (cell, "${v}")
|
|
196
|
+
}
|
|
197
|
+
if (kind == SQLITE_BLOB) {
|
|
198
|
+
let n = Int64(unsafe { sqlite3_column_bytes(stmt, i) })
|
|
199
|
+
cell.put("type", JsonString("blob"))
|
|
200
|
+
cell.put("bytes", JsonInt(n))
|
|
201
|
+
return (cell, "<blob ${n} bytes>")
|
|
202
|
+
}
|
|
203
|
+
if (kind != SQLITE_TEXT) {
|
|
204
|
+
// 兜底:SQLite 只有这几种列类型,走到这里说明是将来才有的形态——如实报 unknown,不冒充文本
|
|
205
|
+
cell.put("type", JsonString("unknown"))
|
|
206
|
+
return (cell, "")
|
|
207
|
+
}
|
|
208
|
+
// TEXT:超上限只给前段并标 truncated + 原始字节数
|
|
209
|
+
let text = sqliteCString(unsafe { sqlite3_column_text(stmt, i) })
|
|
210
|
+
let n = Int64(unsafe { sqlite3_column_bytes(stmt, i) })
|
|
211
|
+
cell.put("type", JsonString("text"))
|
|
212
|
+
if (n > SQLITE_TEXT_CELL_LIMIT) {
|
|
213
|
+
cell.put("value", JsonString(sqliteClampUtf8(text, SQLITE_TEXT_CELL_LIMIT)))
|
|
214
|
+
cell.put("bytes", JsonInt(n))
|
|
215
|
+
cell.put("truncated", JsonBool(true))
|
|
216
|
+
return (cell, sqliteClampUtf8(text, SQLITE_TEXT_CELL_LIMIT))
|
|
217
|
+
}
|
|
218
|
+
cell.put("value", JsonString(text))
|
|
219
|
+
return (cell, text)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 跑一条 SQL 并把结果收下来(最多 maxRows 行,超出置 truncated)。
|
|
224
|
+
*
|
|
225
|
+
* 只预编译**第一条**语句:调试台里写多条时,后面的靠页面再发一次(sqlite3_prepare_v2 就是这个语义)。
|
|
226
|
+
* 语句与 SQL 缓冲的生命周期都在本函数内:缓冲在 prepare 之前分配、finalize 之后才出作用域。
|
|
227
|
+
* 出错抛 `CommandException`(前端拿到可读文案),`finally` 保证语句被 finalize。
|
|
228
|
+
*/
|
|
229
|
+
func sqliteRunQuery(db: CPointer<Unit>, sql: String, maxRows: Int64): SqliteRows {
|
|
230
|
+
let result = SqliteRows()
|
|
231
|
+
let sqlC = unsafe { LibC.mallocCString(sql) }
|
|
232
|
+
var stmt: CPointer<Unit> = CPointer<Unit>()
|
|
233
|
+
let rc = unsafe { sqlite3_prepare_v2(db, sqlC, -1, inout stmt, CPointer<Unit>()) }
|
|
234
|
+
if (rc != SQLITE_OK) {
|
|
235
|
+
throw CommandException("sqlite: prepare failed: ${sqliteLastError(db)} (rc=${rc})")
|
|
236
|
+
}
|
|
237
|
+
if (stmt.isNull()) {
|
|
238
|
+
throw CommandException("sqlite: empty statement")
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
let nCols = unsafe { sqlite3_column_count(stmt) }
|
|
242
|
+
for (i in 0..nCols) {
|
|
243
|
+
result.columns.add(sqliteCString(unsafe { sqlite3_column_name(stmt, i) }))
|
|
244
|
+
}
|
|
245
|
+
while (true) {
|
|
246
|
+
let stepRc = unsafe { sqlite3_step(stmt) }
|
|
247
|
+
if (stepRc == SQLITE_ROW) {
|
|
248
|
+
if (Int64(result.rows.size) >= maxRows) {
|
|
249
|
+
result.truncated = true
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
let rowCells = ArrayList<JsonValue>()
|
|
253
|
+
let rowPlain = ArrayList<String>()
|
|
254
|
+
for (i in 0..nCols) {
|
|
255
|
+
let (cell, plain) = sqliteReadCell(stmt, i)
|
|
256
|
+
rowCells.add(cell)
|
|
257
|
+
rowPlain.add(plain)
|
|
258
|
+
}
|
|
259
|
+
result.rows.add(rowCells)
|
|
260
|
+
result.plain.add(rowPlain)
|
|
261
|
+
} else if (stepRc == SQLITE_DONE) {
|
|
262
|
+
break
|
|
263
|
+
} else {
|
|
264
|
+
throw CommandException("sqlite: step failed: ${sqliteLastError(db)} (rc=${stepRc})")
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
let _ = unsafe { sqlite3_finalize(stmt) }
|
|
269
|
+
}
|
|
270
|
+
return result
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 取一个整数值(COUNT / PRAGMA 这类单值查询);不是整数就给 fallback,不猜 */
|
|
274
|
+
func sqliteScalarInt(db: CPointer<Unit>, sql: String, fallback: Int64): Int64 {
|
|
275
|
+
let sqlC = unsafe { LibC.mallocCString(sql) }
|
|
276
|
+
var stmt: CPointer<Unit> = CPointer<Unit>()
|
|
277
|
+
let rc = unsafe { sqlite3_prepare_v2(db, sqlC, -1, inout stmt, CPointer<Unit>()) }
|
|
278
|
+
if (rc != SQLITE_OK || stmt.isNull()) {
|
|
279
|
+
return fallback
|
|
280
|
+
}
|
|
281
|
+
var out = fallback
|
|
282
|
+
try {
|
|
283
|
+
if (unsafe { sqlite3_step(stmt) } == SQLITE_ROW) {
|
|
284
|
+
if (unsafe { sqlite3_column_type(stmt, 0) } == SQLITE_INTEGER) {
|
|
285
|
+
out = unsafe { sqlite3_column_int64(stmt, 0) }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
let _ = unsafe { sqlite3_finalize(stmt) }
|
|
290
|
+
}
|
|
291
|
+
return out
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** SQL 字符串字面量:单引号翻倍。表名虽已过白名单,仍按字面量正规转义,不靠「反正它安全」 */
|
|
295
|
+
func sqliteQuoteLiteral(s: String): String {
|
|
296
|
+
return "'" + s.replace("'", "''") + "'"
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* sqlite 插件的命令处理器(七个命令共用一份,按全名分派,与 FsCommands / SystemCommands 同形)
|
|
301
|
+
*/
|
|
302
|
+
class SqliteCommands <: CommandHandler {
|
|
303
|
+
public init() {}
|
|
304
|
+
|
|
305
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
306
|
+
match (cmd) {
|
|
307
|
+
case "sqlite:info" => this.info(args, ipc)
|
|
308
|
+
case "sqlite:tables" => this.tables(args, ipc)
|
|
309
|
+
case "sqlite:schema" => this.schema(args, ipc)
|
|
310
|
+
case "sqlite:page" => this.page(args, ipc)
|
|
311
|
+
case "sqlite:query" => this.query(args, ipc)
|
|
312
|
+
case "sqlite:exec" => this.exec(args, ipc)
|
|
313
|
+
case "sqlite:env" => this.env(args, ipc)
|
|
314
|
+
case _ => throw CommandException("unknown sqlite command: ${cmd}")
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 取路径参数并过 scope 校验:越界在**这里**就抛(前端拿到可读错误),
|
|
320
|
+
* 命令体拿到的一定是已经规范化、且落在白名单里的路径。
|
|
321
|
+
*/
|
|
322
|
+
private func scopedPath(args: JsonObject, ipc: IpcContext): String {
|
|
323
|
+
return ipc.resolveScopedPath("sqlite", this.requireString(args, "path"))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 取必需的字符串参数;缺失或类型不对时抛业务错误,前端拿到的是可读原文 */
|
|
327
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
328
|
+
match (args.get(key)) {
|
|
329
|
+
case Some(v) => match (v.kind()) {
|
|
330
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
331
|
+
case _ => throw CommandException("sqlite: argument \"${key}\" must be a string")
|
|
332
|
+
}
|
|
333
|
+
case None => throw CommandException("sqlite: missing argument \"${key}\"")
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 可选整数参数:缺省给 fallback。
|
|
339
|
+
* **给了但不是整数就抛**——静默当缺省会把「页面写错」伪装成「参数没给」,越界分页就白限了。
|
|
340
|
+
*/
|
|
341
|
+
private func optionalInt(args: JsonObject, key: String, fallback: Int64): Int64 {
|
|
342
|
+
match (args.get(key)) {
|
|
343
|
+
case Some(v) => match (v.kind()) {
|
|
344
|
+
case JsonKind.JsInt => return v.asInt().getValue()
|
|
345
|
+
case JsonKind.JsNull => return fallback
|
|
346
|
+
case _ => throw CommandException("sqlite: argument \"${key}\" must be an integer")
|
|
347
|
+
}
|
|
348
|
+
case None => return fallback
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** 可选字符串参数:缺省给 fallback(空串表示「没写」;给了非字符串就抛) */
|
|
353
|
+
private func optionalString(args: JsonObject, key: String, fallback: String): String {
|
|
354
|
+
match (args.get(key)) {
|
|
355
|
+
case Some(v) => match (v.kind()) {
|
|
356
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
357
|
+
case JsonKind.JsNull => return fallback
|
|
358
|
+
case _ => throw CommandException("sqlite: argument \"${key}\" must be a string")
|
|
359
|
+
}
|
|
360
|
+
case None => return fallback
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** 可选布尔参数:缺省给 fallback(页面少传一个 desc 不该报错) */
|
|
365
|
+
private func optionalBool(args: JsonObject, key: String, fallback: Bool): Bool {
|
|
366
|
+
match (args.get(key)) {
|
|
367
|
+
case Some(v) => match (v.kind()) {
|
|
368
|
+
case JsonKind.JsBool => return v.asBool().getValue()
|
|
369
|
+
case JsonKind.JsNull => return fallback
|
|
370
|
+
case _ => throw CommandException("sqlite: argument \"${key}\" must be a boolean")
|
|
371
|
+
}
|
|
372
|
+
case None => return fallback
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 打开一个**只读**连接(浏览类命令统一走这里):文件不存在给可读错误,不顺手造一个空库——
|
|
378
|
+
* 「浏览一个不存在的库」和「新建一个空库」是两件事,后者该由 exec 显式做。
|
|
379
|
+
* 只读是内核级的(SQLITE_OPEN_READONLY),不是我们写的检查。
|
|
380
|
+
*/
|
|
381
|
+
private func openReadOnly(path: String): CPointer<Unit> {
|
|
382
|
+
if (!exists(path)) {
|
|
383
|
+
throw CommandException("sqlite: no such database file: ${path}")
|
|
384
|
+
}
|
|
385
|
+
return sqliteOpenGuarded(path, true)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 本库里全部表 / 视图名:**表名白名单**——页面给的表名必须在其中,之后一律按标识符引用 */
|
|
389
|
+
private func tableNames(db: CPointer<Unit>): ArrayList<String> {
|
|
390
|
+
let res = sqliteRunQuery(db, "SELECT name FROM sqlite_master WHERE type IN ('table','view') " +
|
|
391
|
+
"AND name NOT LIKE 'sqlite_%' ORDER BY name", SQLITE_TABLE_LIMIT)
|
|
392
|
+
let names = ArrayList<String>()
|
|
393
|
+
for (i in 0..res.rows.size) {
|
|
394
|
+
names.add(res.textAt(i, 0))
|
|
395
|
+
}
|
|
396
|
+
return names
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 表名校验:不在白名单里就拒(不把页面给的字符串拼进 SQL) */
|
|
400
|
+
private func requireKnownTable(db: CPointer<Unit>, table: String): String {
|
|
401
|
+
for (name in this.tableNames(db)) {
|
|
402
|
+
if (name == table) {
|
|
403
|
+
return name
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
throw CommandException("sqlite: no such table: ${table}")
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 一次 PRAGMA table_info 同时给出「页面要的列定义」和「内部用的列名」——
|
|
411
|
+
* page 命令两样都要,查两遍纯属浪费(同一个句柄上再来一轮 prepare/step)。
|
|
412
|
+
*/
|
|
413
|
+
private func tableInfo(db: CPointer<Unit>, table: String): (ArrayList<JsonValue>, ArrayList<String>) {
|
|
414
|
+
let res = sqliteRunQuery(db, "PRAGMA table_info(" + sqliteQuoteIdent(table) + ")",
|
|
415
|
+
SQLITE_TABLE_LIMIT)
|
|
416
|
+
let names = ArrayList<String>()
|
|
417
|
+
let cols = ArrayList<JsonValue>()
|
|
418
|
+
let iName = res.columnIndex("name")
|
|
419
|
+
let iType = res.columnIndex("type")
|
|
420
|
+
let iNotNull = res.columnIndex("notnull")
|
|
421
|
+
let iPk = res.columnIndex("pk")
|
|
422
|
+
let iDefault = res.columnIndex("dflt_value")
|
|
423
|
+
for (i in 0..res.rows.size) {
|
|
424
|
+
let o = JsonObject()
|
|
425
|
+
o.put("name", JsonString(res.textAt(i, iName)))
|
|
426
|
+
names.add(res.textAt(i, iName))
|
|
427
|
+
o.put("declType", JsonString(res.textAt(i, iType)))
|
|
428
|
+
o.put("notNull", JsonBool(res.textAt(i, iNotNull) == "1"))
|
|
429
|
+
o.put("pk", JsonBool(res.textAt(i, iPk) != "0"))
|
|
430
|
+
o.put("default", JsonString(res.textAt(i, iDefault)))
|
|
431
|
+
cols.add(o)
|
|
432
|
+
}
|
|
433
|
+
return (cols, names)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** 列定义(页面那一面):只取 JsonArray 用得到的部分 */
|
|
437
|
+
private func columnDefs(db: CPointer<Unit>, table: String): ArrayList<JsonValue> {
|
|
438
|
+
let (cols, _) = this.tableInfo(db, table)
|
|
439
|
+
return cols
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** 建表原文(sqlite_master.sql):调试台常要「把结构抄走」,没有就给空串 */
|
|
443
|
+
private func tableSql(db: CPointer<Unit>, table: String): String {
|
|
444
|
+
let res = sqliteRunQuery(db, "SELECT sql FROM sqlite_master WHERE name = " +
|
|
445
|
+
sqliteQuoteLiteral(table), 1)
|
|
446
|
+
return res.textAt(0, 0)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** `sqlite:info`:这个库是什么、多大、有几个对象——打开一个库后的第一眼 */
|
|
450
|
+
private func info(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
451
|
+
let path = this.scopedPath(args, ipc)
|
|
452
|
+
let db = this.openReadOnly(path)
|
|
453
|
+
try {
|
|
454
|
+
let o = JsonObject()
|
|
455
|
+
o.put("path", JsonString(path))
|
|
456
|
+
o.put("sqliteVersion", JsonString(sqliteLibVersion()))
|
|
457
|
+
o.put("fileBytes", JsonInt(FileInfo(path).size))
|
|
458
|
+
o.put("pageSize", JsonInt(sqliteScalarInt(db, "PRAGMA page_size", -1)))
|
|
459
|
+
o.put("tableCount", JsonInt(sqliteScalarInt(db, "SELECT COUNT(*) FROM sqlite_master " +
|
|
460
|
+
"WHERE type='table' AND name NOT LIKE 'sqlite_%'", -1)))
|
|
461
|
+
o.put("readonly", JsonBool(true))
|
|
462
|
+
o.put("pageMax", JsonInt(SQLITE_PAGE_MAX))
|
|
463
|
+
o.put("cellTextLimit", JsonInt(SQLITE_TEXT_CELL_LIMIT))
|
|
464
|
+
o.put("attachBlocked", JsonBool(true))
|
|
465
|
+
return o
|
|
466
|
+
} finally {
|
|
467
|
+
sqliteCloseQuietly(db)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** `sqlite:tables`:表 / 视图清单,附行数与列数(视图行数报 -1:它没有独立行存储,不拿 0 冒充) */
|
|
472
|
+
private func tables(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
473
|
+
let path = this.scopedPath(args, ipc)
|
|
474
|
+
let db = this.openReadOnly(path)
|
|
475
|
+
try {
|
|
476
|
+
let res = sqliteRunQuery(db, "SELECT name, type FROM sqlite_master " +
|
|
477
|
+
"WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name",
|
|
478
|
+
SQLITE_TABLE_LIMIT)
|
|
479
|
+
let items = ArrayList<JsonValue>()
|
|
480
|
+
for (i in 0..res.rows.size) {
|
|
481
|
+
let name = res.textAt(i, 0)
|
|
482
|
+
let kind = res.textAt(i, 1)
|
|
483
|
+
let o = JsonObject()
|
|
484
|
+
o.put("name", JsonString(name))
|
|
485
|
+
o.put("kind", JsonString(kind))
|
|
486
|
+
if (kind == "table") {
|
|
487
|
+
o.put("rows", JsonInt(sqliteScalarInt(db, "SELECT COUNT(*) FROM " +
|
|
488
|
+
sqliteQuoteIdent(name), -1)))
|
|
489
|
+
} else {
|
|
490
|
+
o.put("rows", JsonInt(-1))
|
|
491
|
+
}
|
|
492
|
+
o.put("columns", JsonInt(this.columnDefs(db, name).size))
|
|
493
|
+
items.add(o)
|
|
494
|
+
}
|
|
495
|
+
let out = JsonObject()
|
|
496
|
+
out.put("tables", JsonArray(items))
|
|
497
|
+
out.put("truncated", JsonBool(res.truncated))
|
|
498
|
+
return out
|
|
499
|
+
} finally {
|
|
500
|
+
sqliteCloseQuietly(db)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** `sqlite:schema`:某张表的列定义 + 索引 + 建表原文(「这张表长什么样」一屏说清) */
|
|
505
|
+
private func schema(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
506
|
+
let path = this.scopedPath(args, ipc)
|
|
507
|
+
let want = this.requireString(args, "table")
|
|
508
|
+
let db = this.openReadOnly(path)
|
|
509
|
+
try {
|
|
510
|
+
let table = this.requireKnownTable(db, want)
|
|
511
|
+
let o = JsonObject()
|
|
512
|
+
o.put("table", JsonString(table))
|
|
513
|
+
o.put("columns", JsonArray(this.columnDefs(db, table)))
|
|
514
|
+
o.put("rowCount", JsonInt(sqliteScalarInt(db, "SELECT COUNT(*) FROM " +
|
|
515
|
+
sqliteQuoteIdent(table), -1)))
|
|
516
|
+
let idxRes = sqliteRunQuery(db, "PRAGMA index_list(" + sqliteQuoteIdent(table) + ")",
|
|
517
|
+
SQLITE_TABLE_LIMIT)
|
|
518
|
+
let iName = idxRes.columnIndex("name")
|
|
519
|
+
let iUnique = idxRes.columnIndex("unique")
|
|
520
|
+
let iOrigin = idxRes.columnIndex("origin")
|
|
521
|
+
let idx = ArrayList<JsonValue>()
|
|
522
|
+
for (i in 0..idxRes.rows.size) {
|
|
523
|
+
let io = JsonObject()
|
|
524
|
+
io.put("name", JsonString(idxRes.textAt(i, iName)))
|
|
525
|
+
io.put("unique", JsonBool(idxRes.textAt(i, iUnique) == "1"))
|
|
526
|
+
io.put("origin", JsonString(idxRes.textAt(i, iOrigin)))
|
|
527
|
+
idx.add(io)
|
|
528
|
+
}
|
|
529
|
+
o.put("indexes", JsonArray(idx))
|
|
530
|
+
o.put("sql", JsonString(this.tableSql(db, table)))
|
|
531
|
+
return o
|
|
532
|
+
} finally {
|
|
533
|
+
sqliteCloseQuietly(db)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** 单页行数夹紧:页面要多了就夹到上限,回投里带 limitClamped 如实说明(不静默改数) */
|
|
538
|
+
private func clampLimit(want: Int64): Int64 {
|
|
539
|
+
if (want <= 0 || want > SQLITE_PAGE_MAX) {
|
|
540
|
+
return SQLITE_PAGE_MAX
|
|
541
|
+
}
|
|
542
|
+
return want
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** 名字在不在白名单里(表名 / 列名共用一个判定) */
|
|
546
|
+
private func containsName(names: ArrayList<String>, want: String): Bool {
|
|
547
|
+
for (n in names) {
|
|
548
|
+
if (n == want) {
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** 把行摊平成 JSON 二维数组:行序就是 SQL 给的顺序,不重排(要稳定顺序自己带 ORDER BY) */
|
|
556
|
+
private func rowsToJson(res: SqliteRows): ArrayList<JsonValue> {
|
|
557
|
+
let rows = ArrayList<JsonValue>()
|
|
558
|
+
for (r in res.rows) {
|
|
559
|
+
let arr = ArrayList<JsonValue>()
|
|
560
|
+
for (cell in r) {
|
|
561
|
+
arr.add(cell)
|
|
562
|
+
}
|
|
563
|
+
rows.add(JsonArray(arr))
|
|
564
|
+
}
|
|
565
|
+
return rows
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* `sqlite:page`:分页取数(浏览表格的正路)。
|
|
570
|
+
* 表名过白名单、排序列过列名白名单、offset / limit 是整数——**页面给的字符串一个都不拼进 SQL**。
|
|
571
|
+
* 没写 ORDER BY 时按 SQLite 给的顺序回,不做「稳定排序」的假承诺(要稳定自己带列名)。
|
|
572
|
+
*/
|
|
573
|
+
private func page(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
574
|
+
let path = this.scopedPath(args, ipc)
|
|
575
|
+
let want = this.requireString(args, "table")
|
|
576
|
+
let offset = this.optionalInt(args, "offset", 0)
|
|
577
|
+
let wantLimit = this.optionalInt(args, "limit", SQLITE_PAGE_MAX)
|
|
578
|
+
let limit = this.clampLimit(wantLimit)
|
|
579
|
+
let orderBy = this.optionalString(args, "orderBy", "")
|
|
580
|
+
let desc = this.optionalBool(args, "desc", false)
|
|
581
|
+
if (offset < 0) {
|
|
582
|
+
throw CommandException("sqlite: argument \"offset\" must not be negative")
|
|
583
|
+
}
|
|
584
|
+
let db = this.openReadOnly(path)
|
|
585
|
+
try {
|
|
586
|
+
let table = this.requireKnownTable(db, want)
|
|
587
|
+
let (cols, colNames) = this.tableInfo(db, table)
|
|
588
|
+
var sql = "SELECT * FROM " + sqliteQuoteIdent(table)
|
|
589
|
+
if (orderBy != "") {
|
|
590
|
+
if (!this.containsName(colNames, orderBy)) {
|
|
591
|
+
throw CommandException("sqlite: no such column in ${table}: ${orderBy}")
|
|
592
|
+
}
|
|
593
|
+
sql = sql + " ORDER BY " + sqliteQuoteIdent(orderBy)
|
|
594
|
+
if (desc) {
|
|
595
|
+
sql = sql + " DESC"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
sql = sql + " LIMIT ${limit} OFFSET ${offset}"
|
|
599
|
+
let res = sqliteRunQuery(db, sql, limit)
|
|
600
|
+
let o = JsonObject()
|
|
601
|
+
o.put("table", JsonString(table))
|
|
602
|
+
o.put("offset", JsonInt(offset))
|
|
603
|
+
o.put("limit", JsonInt(limit))
|
|
604
|
+
o.put("limitClamped", JsonBool(wantLimit != limit))
|
|
605
|
+
o.put("total", JsonInt(sqliteScalarInt(db, "SELECT COUNT(*) FROM " +
|
|
606
|
+
sqliteQuoteIdent(table), -1)))
|
|
607
|
+
o.put("columns", JsonArray(cols))
|
|
608
|
+
o.put("rows", JsonArray(this.rowsToJson(res)))
|
|
609
|
+
o.put("truncated", JsonBool(res.truncated))
|
|
610
|
+
return o
|
|
611
|
+
} finally {
|
|
612
|
+
sqliteCloseQuietly(db)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* `sqlite:query`:手写 SQL(调试台的正路)。连接是**只读**的——写语句在这里会以只读错误失败,
|
|
618
|
+
* 要写走 exec:「手滑跑了一条 DELETE」不该发生在一个看数据的界面上(D-018 第 5 条)。
|
|
619
|
+
* 只跑第一条语句(sqlite3_prepare_v2 的语义),多条要分次发。
|
|
620
|
+
*/
|
|
621
|
+
private func query(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
622
|
+
let path = this.scopedPath(args, ipc)
|
|
623
|
+
let sql = this.requireString(args, "sql")
|
|
624
|
+
let wantLimit = this.optionalInt(args, "limit", SQLITE_PAGE_MAX)
|
|
625
|
+
let limit = this.clampLimit(wantLimit)
|
|
626
|
+
let db = this.openReadOnly(path)
|
|
627
|
+
try {
|
|
628
|
+
let res = sqliteRunQuery(db, sql, limit)
|
|
629
|
+
let cols = ArrayList<JsonValue>()
|
|
630
|
+
for (name in res.columns) {
|
|
631
|
+
let co = JsonObject()
|
|
632
|
+
co.put("name", JsonString(name))
|
|
633
|
+
cols.add(co)
|
|
634
|
+
}
|
|
635
|
+
let o = JsonObject()
|
|
636
|
+
o.put("columns", JsonArray(cols))
|
|
637
|
+
o.put("rows", JsonArray(this.rowsToJson(res)))
|
|
638
|
+
o.put("rowCount", JsonInt(res.rows.size))
|
|
639
|
+
o.put("truncated", JsonBool(res.truncated))
|
|
640
|
+
o.put("limit", JsonInt(limit))
|
|
641
|
+
o.put("limitClamped", JsonBool(wantLimit != limit))
|
|
642
|
+
return o
|
|
643
|
+
} finally {
|
|
644
|
+
sqliteCloseQuietly(db)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* `sqlite:exec`:唯一有写能力的一条(`readonly` 权限集里没有它,这也正是分两个集的原因)。
|
|
650
|
+
* 走 sqlite3_exec:库文件不存在时**在这里**显式建(浏览类命令不会顺手造库)。
|
|
651
|
+
* `ATTACH` / `load_extension()` 会被 authorizer 挡下,报错文案是 `not authorized`,
|
|
652
|
+
* 同时桥 stderr 上留一行 `sqlite denied: …`——拒绝必须留痕。
|
|
653
|
+
*/
|
|
654
|
+
private func exec(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
655
|
+
let path = this.scopedPath(args, ipc)
|
|
656
|
+
let sql = this.requireString(args, "sql")
|
|
657
|
+
let db = sqliteOpenGuarded(path, false)
|
|
658
|
+
try {
|
|
659
|
+
let sqlC = unsafe { LibC.mallocCString(sql) }
|
|
660
|
+
let rc = unsafe {
|
|
661
|
+
sqlite3_exec(db, sqlC, CPointer<Unit>(), CPointer<Unit>(), CPointer<Unit>())
|
|
662
|
+
}
|
|
663
|
+
if (rc != SQLITE_OK) {
|
|
664
|
+
// 被插件护栏挡下的语句给可读文案:页面看到的该是「这条不许跑」而不是一串返回码
|
|
665
|
+
if (rc == SQLITE_AUTH) {
|
|
666
|
+
throw CommandException("sqlite: exec refused by plugin guard " +
|
|
667
|
+
"(attach / detach / load_extension are not allowed)")
|
|
668
|
+
}
|
|
669
|
+
throw CommandException("sqlite: exec failed (rc=${rc}): ${sqliteLastError(db)}")
|
|
670
|
+
}
|
|
671
|
+
let o = JsonObject()
|
|
672
|
+
o.put("changes", JsonInt(Int64(unsafe { sqlite3_changes(db) })))
|
|
673
|
+
o.put("lastInsertRowid", JsonInt(unsafe { sqlite3_last_insert_rowid(db) }))
|
|
674
|
+
return o
|
|
675
|
+
} finally {
|
|
676
|
+
sqliteCloseQuietly(db)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* `sqlite:env`:把自检开关与本轮夹具库路径交给页面(内联页在 WebView 里,读不到进程环境)。
|
|
682
|
+
* **不绕过 scope**:页面拿到路径后仍要把它当普通 `path` 参数发给别的命令,该过的校验一次不少。
|
|
683
|
+
*/
|
|
684
|
+
private func env(args: JsonObject, ipc: IpcContext): JsonValue {
|
|
685
|
+
let _ = args
|
|
686
|
+
let _ = ipc
|
|
687
|
+
let o = JsonObject()
|
|
688
|
+
o.put("selfCheck", JsonBool(this.envOr(SQLITE_ENV_SELFCHECK, "") == "1"))
|
|
689
|
+
o.put("fixture", JsonString(this.envOr(SQLITE_ENV_FIXTURE, "")))
|
|
690
|
+
o.put("cwd", JsonString(scopeCwdPath()))
|
|
691
|
+
o.put("pageMax", JsonInt(SQLITE_PAGE_MAX))
|
|
692
|
+
o.put("cellTextLimit", JsonInt(SQLITE_TEXT_CELL_LIMIT))
|
|
693
|
+
return o
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** 读环境变量,空串当没设(与 src/app.cj 的 manifestDumpMode 同一口径) */
|
|
697
|
+
private func envOr(name: String, fallback: String): String {
|
|
698
|
+
if (let Some(v) <- getVariable(name)) {
|
|
699
|
+
if (v.size > 0) {
|
|
700
|
+
return v
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return fallback
|
|
704
|
+
}
|
|
705
|
+
}
|